Compare commits
20 Commits
6d8a014cc7
...
aced0a63c9
Author | SHA1 | Date |
---|---|---|
Shadowfacts | aced0a63c9 | |
Shadowfacts | 1e54235ff5 | |
Shadowfacts | e6e5554edf | |
Shadowfacts | 9026f487ec | |
Shadowfacts | c0097ba752 | |
Shadowfacts | f109253bba | |
Shadowfacts | 1fda4248ec | |
Shadowfacts | 7781c5252b | |
Shadowfacts | 7f4bf52050 | |
Shadowfacts | ba0d179de5 | |
Shadowfacts | 71b6f1bdf0 | |
Shadowfacts | 09ec4a920c | |
Shadowfacts | 7edf0fdb93 | |
Shadowfacts | 99e06441f0 | |
Shadowfacts | 85e1e131f6 | |
Shadowfacts | 1d79918a94 | |
Shadowfacts | 340d13b1fa | |
Shadowfacts | cf1000a4df | |
Shadowfacts | b781b56efd | |
Shadowfacts | 10a8a85bfc |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2021.1 (22)
|
||||||
|
This is the first public beta build of Tusker, so if you're just joining us, welcome! Not too many new features this build, mostly bugfixes, so test everything and generally use the app.
|
||||||
|
|
||||||
|
Features/Improvements:
|
||||||
|
- Add timeline descriptions the first time you view federated/local
|
||||||
|
- Show messages when loading posts fails or when there are no newer posts
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash after editing lists
|
||||||
|
- Fix crash when refreshing before anything is loaded
|
||||||
|
- Fix crash when fetching recommended instances fails
|
||||||
|
- Fix crash when replying to posts with code formatting
|
||||||
|
- Fix crash when changing preferences after switching accounts
|
||||||
|
|
||||||
## 2021.1 (21)
|
## 2021.1 (21)
|
||||||
This is a quick follow-up to the previous build with fixes for a couple major crashes. Unfortunately, due to a bug in iOS 14, the Disable Infinite Scrolling preference now requires the iOS 15 beta to use. It may return in a future build if I can find a workaround, but it's disabled in the meantime.
|
This is a quick follow-up to the previous build with fixes for a couple major crashes. Unfortunately, due to a bug in iOS 14, the Disable Infinite Scrolling preference now requires the iOS 15 beta to use. It may return in a future build if I can find a workaround, but it's disabled in the meantime.
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
|
||||||
<string>Action</string>
|
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
||||||
|
@ -35,6 +33,8 @@
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||||
|
<string>Action</string>
|
||||||
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class NotificationGroup {
|
public class NotificationGroup: Identifiable, Hashable {
|
||||||
public let notifications: [Notification]
|
public let notifications: [Notification]
|
||||||
public let id: String
|
public let id: String
|
||||||
public let kind: Notification.Kind
|
public let kind: Notification.Kind
|
||||||
|
@ -26,6 +26,14 @@ public class NotificationGroup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func ==(lhs: NotificationGroup, rhs: NotificationGroup) -> Bool {
|
||||||
|
return lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
public static func createGroups(notifications: [Notification], only allowedTypes: [Notification.Kind]) -> [NotificationGroup] {
|
||||||
var groups = [[Notification]]()
|
var groups = [[Notification]]()
|
||||||
for notification in notifications {
|
for notification in notifications {
|
||||||
|
@ -50,5 +58,3 @@ public class NotificationGroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationGroup: Identifiable {}
|
|
||||||
|
|
|
@ -134,11 +134,16 @@
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */; };
|
||||||
|
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */; };
|
||||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
D64BC18623C1253A000D0238 /* AssetPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */; };
|
||||||
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.swift */; };
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */; };
|
||||||
|
@ -535,11 +540,16 @@
|
||||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
|
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PublicTimelineDescriptionTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
||||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
D64BC18523C1253A000D0238 /* AssetPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPreviewViewController.swift; sourceTree = "<group>"; };
|
||||||
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18723C1640A000D0238 /* PinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
D64BC18923C16487000D0238 /* UnpinStatusActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnpinStatusActivity.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1058,27 +1068,27 @@
|
||||||
D641C780213DD7C4004B4513 /* Screens */ = {
|
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
|
||||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
|
||||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
|
||||||
D641C781213DD7DD004B4513 /* Timeline */,
|
|
||||||
D641C784213DD819004B4513 /* Profile */,
|
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
|
||||||
D641C786213DD852004B4513 /* Notifications */,
|
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
|
||||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||||
|
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||||
|
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||||
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D627FF77217E94F200CC0648 /* Drafts */,
|
D627FF77217E94F200CC0648 /* Drafts */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
D641C788213DD86D004B4513 /* Large Image */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
D641C786213DD852004B4513 /* Notifications */,
|
||||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||||
D641C789213DD87E004B4513 /* Preferences */,
|
D641C789213DD87E004B4513 /* Preferences */,
|
||||||
|
D641C784213DD819004B4513 /* Profile */,
|
||||||
|
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||||
|
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||||
|
D641C781213DD7DD004B4513 /* Timeline */,
|
||||||
|
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||||
);
|
);
|
||||||
path = Screens;
|
path = Screens;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1240,6 +1250,15 @@
|
||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6420AEB26BED17500ED8175 /* Timeline Description Cell */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */,
|
||||||
|
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = "Timeline Description Cell";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D646C954213B364600269FB5 /* Transitions */ = {
|
D646C954213B364600269FB5 /* Transitions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1250,6 +1269,16 @@
|
||||||
path = Transitions;
|
path = Transitions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */,
|
||||||
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */,
|
||||||
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Toast;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D65A37F221472F300087646E /* Frameworks */ = {
|
D65A37F221472F300087646E /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1441,35 +1470,37 @@
|
||||||
D6BED1722126661300F02DA0 /* Views */ = {
|
D6BED1722126661300F02DA0 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
|
||||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||||
|
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||||
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||||
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||||
|
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
||||||
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
|
||||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
|
||||||
D641C78A213DD926004B4513 /* Status */,
|
|
||||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||||
D623A53B2635F4E20095BD04 /* Poll */,
|
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
D641C78C213DD937004B4513 /* Notifications */,
|
|
||||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
|
||||||
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
|
||||||
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
D61AC1DA232EA43100C54D2D /* Instance Cell */,
|
||||||
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
D641C78C213DD937004B4513 /* Notifications */,
|
||||||
|
D623A53B2635F4E20095BD04 /* Poll */,
|
||||||
|
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||||
|
D641C78A213DD926004B4513 /* Status */,
|
||||||
|
D64AAE8F26C80DB600FC57FB /* Toast */,
|
||||||
|
D6420AEB26BED17500ED8175 /* Timeline Description Cell */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1477,24 +1508,24 @@
|
||||||
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
|
||||||
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
|
||||||
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */,
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
|
||||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
|
||||||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift */,
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||||
|
D6E0DC8D216EDF1E00369478 /* Previewing.swift */,
|
||||||
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||||
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||||
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
D6A6C10425B6138A00298D0F /* StatusTablePrefetching.swift */,
|
||||||
|
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
||||||
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
|
||||||
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
|
||||||
|
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||||
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1541,36 +1572,36 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
||||||
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
||||||
|
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
||||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
||||||
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||||
|
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||||
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
D6DFC69F242C4CCC00ACC392 /* WeakArray.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
|
||||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
|
||||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
|
||||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
|
||||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
|
||||||
D6370B9924421FE00092A7FF /* CoreData */,
|
|
||||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
|
||||||
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
|
||||||
|
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||||
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
|
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||||
|
D6370B9924421FE00092A7FF /* CoreData */,
|
||||||
|
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||||
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
||||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||||
);
|
);
|
||||||
path = Tusker;
|
path = Tusker;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1886,6 +1917,7 @@
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
|
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */,
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
|
@ -2052,6 +2084,7 @@
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||||
|
@ -2133,6 +2166,7 @@
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||||
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
@ -2147,6 +2181,8 @@
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||||
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
|
@ -2509,6 +2545,7 @@
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -2565,6 +2602,7 @@
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||||
VALIDATE_PRODUCT = YES;
|
VALIDATE_PRODUCT = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
@ -2577,7 +2615,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2610,7 +2648,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2719,7 +2757,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2746,7 +2784,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 21;
|
CURRENT_PROJECT_VERSION = 22;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -67,6 +67,9 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
|
||||||
|
self.hasShownLocalTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownLocalTimelineDescription) ?? false
|
||||||
|
self.hasShownFederatedTimelineDescription = try container.decodeIfPresent(Bool.self, forKey: .hasShownFederatedTimelineDescription) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
|
@ -102,6 +105,9 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
try container.encode(silentActions, forKey: .silentActions)
|
try container.encode(silentActions, forKey: .silentActions)
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
|
||||||
|
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
||||||
|
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Appearance
|
// MARK: Appearance
|
||||||
|
@ -141,6 +147,10 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var silentActions: [String: Permission] = [:]
|
@Published var silentActions: [String: Permission] = [:]
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
|
|
||||||
|
// MARK:
|
||||||
|
@Published var hasShownLocalTimelineDescription = false
|
||||||
|
@Published var hasShownFederatedTimelineDescription = false
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case theme
|
case theme
|
||||||
case avatarStyle
|
case avatarStyle
|
||||||
|
@ -172,6 +182,9 @@ class Preferences: Codable, ObservableObject {
|
||||||
|
|
||||||
case silentActions
|
case silentActions
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
||||||
|
case hasShownLocalTimelineDescription
|
||||||
|
case hasShownFederatedTimelineDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,12 +73,12 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
accountView.alpha = 0
|
accountView.alpha = 0
|
||||||
accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
accountView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
|
||||||
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration) {
|
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration / 2) {
|
||||||
accountView.alpha = 1
|
accountView.alpha = 1
|
||||||
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
||||||
}
|
}
|
||||||
|
|
||||||
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration, relativeDuration: relDuration) {
|
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration / 2, relativeDuration: relDuration / 2) {
|
||||||
accountView.transform = .identity
|
accountView.transform = .identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,3 +89,11 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
root.performSearch(query: query)
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||||
|
func sceneDidEnterBackground() {
|
||||||
|
if let backgroundable = root as? BackgroundableViewController {
|
||||||
|
backgroundable.sceneDidEnterBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
|
||||||
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
|
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> {
|
||||||
|
|
||||||
private let statusCell = "statusCell"
|
private let statusCell = "statusCell"
|
||||||
private let actionGroupCell = "actionGroupCell"
|
private let actionGroupCell = "actionGroupCell"
|
||||||
|
@ -54,88 +54,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) {
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(notifications, pagination) = response else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadOlder(completion: @escaping ([NotificationGroup]) -> Void) {
|
|
||||||
guard let older = older else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadNewer(completion: @escaping ([NotificationGroup]) -> Void) {
|
|
||||||
guard let newer = newer else {
|
|
||||||
completion([])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
guard case let .success(newNotifications, pagination) = response else { fatalError() }
|
|
||||||
|
|
||||||
if let newer = pagination?.newer {
|
|
||||||
self.newer = newer
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
|
||||||
completion(groups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
item(for: indexPath).notifications
|
|
||||||
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
|
||||||
.forEach { (request) in
|
|
||||||
group.enter()
|
|
||||||
mastodonController.run(request) { (_) in
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.notify(queue: .main) {
|
|
||||||
self.sections[indexPath.section].remove(at: indexPath.row)
|
|
||||||
self.tableView.deleteRows(at: [indexPath], with: .automatic)
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let group = item(for: indexPath)
|
|
||||||
|
|
||||||
|
override func cellProvider(_ tableView: UITableView, _ indexPath: IndexPath, _ group: NotificationGroup) -> UITableViewCell? {
|
||||||
switch group.kind {
|
switch group.kind {
|
||||||
case .mention:
|
case .mention:
|
||||||
guard let notification = group.notifications.first,
|
guard let notification = group.notifications.first,
|
||||||
|
@ -179,6 +100,112 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func loadInitialItems(completion: @escaping (LoadResult) -> Void) {
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(notifications, pagination):
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: notifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.newer = pagination?.newer
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: notifications) {
|
||||||
|
var snapshot = Snapshot()
|
||||||
|
snapshot.appendSections([.notifications])
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let older = older else {
|
||||||
|
completion(.failure(.noOlder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: older)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(newNotifications, pagination):
|
||||||
|
if let older = pagination?.older {
|
||||||
|
self.older = older
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
|
guard let newer = newer else {
|
||||||
|
completion(.failure(.noNewer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Client.getNotifications(excludeTypes: excludedTypes, range: newer)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
|
case let .success(newNotifications, pagination):
|
||||||
|
guard !newNotifications.isEmpty else {
|
||||||
|
completion(.failure(.allCaughtUp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
let groups = NotificationGroup.createGroups(notifications: newNotifications, only: self.groupTypes)
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(notifications: newNotifications) {
|
||||||
|
var snapshot = currentSnapshot()
|
||||||
|
if let first = snapshot.itemIdentifiers(inSection: .notifications).first {
|
||||||
|
snapshot.insertItems(groups, beforeItem: first)
|
||||||
|
} else {
|
||||||
|
snapshot.appendItems(groups, toSection: .notifications)
|
||||||
|
}
|
||||||
|
completion(.success(snapshot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismissNotificationsInGroup(at indexPath: IndexPath, completion: (() -> Void)? = nil) {
|
||||||
|
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
let group = DispatchGroup()
|
||||||
|
item.notifications
|
||||||
|
.map { Pachyderm.Notification.dismiss(id: $0.id) }
|
||||||
|
.forEach { (request) in
|
||||||
|
group.enter()
|
||||||
|
mastodonController.run(request) { (_) in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group.notify(queue: .main) {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteItems([item])
|
||||||
|
self.dataSource.apply(snapshot, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||||
|
@ -211,6 +238,12 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NotificationsTableViewController {
|
||||||
|
enum Section: CaseIterable, Hashable {
|
||||||
|
case notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
extension NotificationsTableViewController: TuskerNavigationDelegate {
|
||||||
var apiController: MastodonController { mastodonController }
|
var apiController: MastodonController { mastodonController }
|
||||||
}
|
}
|
||||||
|
@ -224,7 +257,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
||||||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
|
||||||
|
for notification in group.notifications {
|
||||||
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
|
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,7 +266,8 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
for indexPath in indexPaths {
|
for indexPath in indexPaths {
|
||||||
for notification in item(for: indexPath).notifications {
|
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
|
||||||
|
for notification in group.notifications {
|
||||||
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
var urlHandler: AnyCancellable?
|
var urlHandler: AnyCancellable?
|
||||||
var currentQuery: String?
|
var currentQuery: String?
|
||||||
|
|
||||||
|
private var activityIndicator: UIActivityIndicatorView!
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
return .portrait
|
return .portrait
|
||||||
|
@ -50,10 +52,15 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
// disable transparent background when scrolled to top because it gets weird with animating table items in and out
|
||||||
|
let appearance = UINavigationBarAppearance()
|
||||||
|
appearance.configureWithDefaultBackground()
|
||||||
|
navigationItem.scrollEdgeAppearance = appearance
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "InstanceTableViewCell", bundle: .main), forCellReuseIdentifier: instanceCell)
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 120
|
tableView.estimatedRowHeight = 120
|
||||||
|
createActivityIndicatorHeader()
|
||||||
|
|
||||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -73,12 +80,19 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
searchController.obscuresBackgroundDuringPresentation = false
|
searchController.obscuresBackgroundDuringPresentation = false
|
||||||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||||
navigationItem.searchController = searchController
|
navigationItem.searchController = searchController
|
||||||
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
definesPresentationContext = true
|
definesPresentationContext = true
|
||||||
|
|
||||||
urlHandler = urlCheckerSubject
|
urlHandler = urlCheckerSubject
|
||||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
|
||||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
.sink(receiveValue: updateSpecificInstance)
|
.map { [weak self] (s) -> String in
|
||||||
|
if !s.isEmpty {
|
||||||
|
self?.activityIndicator.startAnimating()
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||||
|
.sink { [weak self] in self?.updateSpecificInstance(domain: $0) }
|
||||||
|
|
||||||
loadRecommendedInstances()
|
loadRecommendedInstances()
|
||||||
}
|
}
|
||||||
|
@ -112,6 +126,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpecificInstance(domain: String) {
|
private func updateSpecificInstance(domain: String) {
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
let components = parseURLComponents(input: domain)
|
let components = parseURLComponents(input: domain)
|
||||||
let url = components.url!
|
let url = components.url!
|
||||||
|
|
||||||
|
@ -120,16 +136,26 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
client.run(request) { (response) in
|
client.run(request) { (response) in
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
if snapshot.indexOfSection(.selected) != nil {
|
if snapshot.indexOfSection(.selected) != nil {
|
||||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
snapshot.deleteSections([.selected])
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .success(instance, _) = response {
|
if case let .success(instance, _) = response {
|
||||||
if !snapshot.sectionIdentifiers.contains(.selected) {
|
if snapshot.indexOfSection(.recommendedInstances) != nil {
|
||||||
|
snapshot.insertSections([.selected], beforeSection: .recommendedInstances)
|
||||||
|
} else {
|
||||||
snapshot.appendSections([.selected])
|
snapshot.appendSections([.selected])
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
|
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot) {
|
||||||
|
self.activityIndicator.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.activityIndicator.stopAnimating()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,14 +163,69 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
|
|
||||||
private func loadRecommendedInstances() {
|
private func loadRecommendedInstances() {
|
||||||
InstanceSelector.getInstances(category: nil) { (response) in
|
InstanceSelector.getInstances(category: nil) { (response) in
|
||||||
guard case let .success(instances, _) = response else { fatalError() }
|
DispatchQueue.main.async {
|
||||||
|
switch response {
|
||||||
|
case let .failure(error):
|
||||||
|
self.showRecommendationsError(error)
|
||||||
|
case let .success(instances, _):
|
||||||
self.recommendedInstances = instances
|
self.recommendedInstances = instances
|
||||||
self.filterRecommendedResults()
|
self.filterRecommendedResults()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func filterRecommendedResults() {
|
private func createActivityIndicatorHeader() {
|
||||||
|
let header = UITableViewHeaderFooterView()
|
||||||
|
header.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
header.contentView.backgroundColor = .secondarySystemBackground
|
||||||
|
|
||||||
|
activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
header.contentView.addSubview(activityIndicator)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicator.centerXAnchor.constraint(equalTo: header.contentView.centerXAnchor),
|
||||||
|
activityIndicator.topAnchor.constraint(equalTo: header.contentView.topAnchor, constant: 4),
|
||||||
|
activityIndicator.bottomAnchor.constraint(equalTo: header.contentView.bottomAnchor, constant: -4),
|
||||||
|
])
|
||||||
|
|
||||||
|
let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
|
||||||
|
let size = header.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
header.frame = CGRect(origin: .zero, size: size)
|
||||||
|
|
||||||
|
tableView.tableHeaderView = header
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showRecommendationsError(_ error: Client.Error) {
|
||||||
|
let footer = UITableViewHeaderFooterView()
|
||||||
|
footer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.font = .boldSystemFont(ofSize: 17)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.text = "Could not fetch suggested instances: \(error.localizedDescription)"
|
||||||
|
|
||||||
|
footer.contentView.addSubview(label)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalToSystemSpacingAfter: footer.contentView.leadingAnchor, multiplier: 1),
|
||||||
|
footer.contentView.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1),
|
||||||
|
label.topAnchor.constraint(equalTo: footer.contentView.topAnchor, constant: 8),
|
||||||
|
label.bottomAnchor.constraint(equalTo: footer.contentView.bottomAnchor, constant: 8),
|
||||||
|
])
|
||||||
|
|
||||||
|
let fittingSize = CGSize(width: tableView.bounds.width - (tableView.safeAreaInsets.left + tableView.safeAreaInsets.right), height: 0)
|
||||||
|
let size = footer.systemLayoutSizeFitting(fittingSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
footer.frame = CGRect(origin: .zero, size: size)
|
||||||
|
|
||||||
|
tableView.tableFooterView = footer
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filterRecommendedResults() {
|
||||||
let filteredInstances: [InstanceSelector.Instance]
|
let filteredInstances: [InstanceSelector.Instance]
|
||||||
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
||||||
filteredInstances = recommendedInstances.filter {
|
filteredInstances = recommendedInstances.filter {
|
||||||
|
@ -155,13 +236,21 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = self.dataSource.snapshot()
|
var snapshot = self.dataSource.snapshot()
|
||||||
snapshot.deleteSections([.recommendedInstances])
|
if snapshot.indexOfSection(.recommendedInstances) != nil {
|
||||||
snapshot.appendSections([.recommendedInstances])
|
let toRemove = snapshot.itemIdentifiers(inSection: .recommendedInstances).filter {
|
||||||
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
if case .recommended(_) = $0 {
|
||||||
DispatchQueue.main.async {
|
return true
|
||||||
self.dataSource.apply(snapshot)
|
} else {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
snapshot.deleteItems(toRemove)
|
||||||
|
} else {
|
||||||
|
snapshot.appendSections([.recommendedInstances])
|
||||||
|
}
|
||||||
|
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Table view delegate
|
// MARK: - Table view delegate
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
@ -194,29 +283,30 @@ extension InstanceSelectorTableViewController {
|
||||||
case recommended(InstanceSelector.Instance)
|
case recommended(InstanceSelector.Instance)
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
if case let .selected(url, instance) = lhs,
|
switch (lhs, rhs) {
|
||||||
case let .selected(otherUrl, other) = rhs {
|
case let (.selected(urlA, instanceA), .selected(urlB, instanceB)):
|
||||||
return url == otherUrl && instance.uri == other.uri
|
return urlA == urlB && instanceA.uri == instanceB.uri
|
||||||
} else if case let .recommended(instance) = lhs,
|
case let (.recommended(a), .recommended(b)):
|
||||||
case let .recommended(other) = rhs {
|
return a.domain == b.domain
|
||||||
return instance.domain == other.domain
|
default:
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
switch self {
|
switch self {
|
||||||
case let .selected(url, instance):
|
case let .selected(url, instance):
|
||||||
hasher.combine(Section.selected)
|
hasher.combine(0)
|
||||||
hasher.combine(url)
|
hasher.combine(url)
|
||||||
hasher.combine(instance.uri)
|
hasher.combine(instance.uri)
|
||||||
case let .recommended(instance):
|
case let .recommended(instance):
|
||||||
hasher.combine(Section.recommendedInstances)
|
hasher.combine(1)
|
||||||
hasher.combine(instance.domain)
|
hasher.combine(instance.domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,8 @@ class ProfileViewController: UIPageViewController {
|
||||||
|
|
||||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
||||||
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
composeButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
|
||||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
|
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] (_) in
|
||||||
self.composeDirectMentioning()
|
self?.composeDirectMentioning()
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
composeButton.isEnabled = mastodonController.loggedIn
|
composeButton.isEnabled = mastodonController.loggedIn
|
||||||
|
|
|
@ -20,6 +20,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
private var older: RequestRange?
|
private var older: RequestRange?
|
||||||
|
|
||||||
private var didConfirmLoadMore = false
|
private var didConfirmLoadMore = false
|
||||||
|
private var isShowingTimelineDescription = false
|
||||||
|
|
||||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||||
self.timeline = timeline
|
self.timeline = timeline
|
||||||
|
@ -58,6 +59,42 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||||
tableView.register(UINib(nibName: "ConfirmLoadMoreTableViewCell", bundle: .main), forCellReuseIdentifier: "confirmLoadMoreCell")
|
tableView.register(UINib(nibName: "ConfirmLoadMoreTableViewCell", bundle: .main), forCellReuseIdentifier: "confirmLoadMoreCell")
|
||||||
|
tableView.register(UINib(nibName: "PublicTimelineDescriptionTableViewCell", bundle: .main), forCellReuseIdentifier: "publicTimelineDescriptionCell")
|
||||||
|
|
||||||
|
if case let .public(local: local) = timeline,
|
||||||
|
(local && !Preferences.shared.hasShownLocalTimelineDescription) || (!local && !Preferences.shared.hasShownFederatedTimelineDescription) {
|
||||||
|
isShowingTimelineDescription = true
|
||||||
|
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.appendSections([.header])
|
||||||
|
snapshot.appendItems([.publicTimelineDescription(local: local)], toSection: .header)
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if case let .public(local: local) = timeline {
|
||||||
|
if local {
|
||||||
|
Preferences.shared.hasShownLocalTimelineDescription = true
|
||||||
|
} else {
|
||||||
|
Preferences.shared.hasShownFederatedTimelineDescription = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
|
if isShowingTimelineDescription {
|
||||||
|
isShowingTimelineDescription = false
|
||||||
|
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - DiffableTimelineLikeTableViewController
|
// MARK: - DiffableTimelineLikeTableViewController
|
||||||
|
@ -87,6 +124,17 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
self.didConfirmLoadMore = false
|
self.didConfirmLoadMore = false
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
|
case .publicTimelineDescription(local: let local):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "publicTimelineDescriptionCell", for: indexPath) as! PublicTimelineDescriptionTableViewCell
|
||||||
|
cell.mastodonController = mastodonController
|
||||||
|
cell.local = local
|
||||||
|
cell.didDismiss = { [unowned self] in
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +156,9 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
var snapshot = Snapshot()
|
DispatchQueue.main.async {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.statuses, .footer])
|
||||||
snapshot.appendSections([.statuses, .footer])
|
snapshot.appendSections([.statuses, .footer])
|
||||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
completion(.success(snapshot))
|
completion(.success(snapshot))
|
||||||
|
@ -116,8 +166,9 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let older = older else {
|
guard let older = older else {
|
||||||
completion(.failure(.noOlder))
|
completion(.failure(.noOlder))
|
||||||
return
|
return
|
||||||
|
@ -125,12 +176,12 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
|
|
||||||
if #available(iOS 15.0, *),
|
if #available(iOS 15.0, *),
|
||||||
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
|
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
|
||||||
guard !currentSnapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
var snapshot = currentSnapshot()
|
||||||
|
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
|
||||||
// todo: need something more accurate than "success"/"failure"
|
// todo: need something more accurate than "success"/"failure"
|
||||||
completion(.success(currentSnapshot))
|
completion(.success(snapshot))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var snapshot = currentSnapshot
|
|
||||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||||
self.dataSource.apply(snapshot)
|
self.dataSource.apply(snapshot)
|
||||||
completion(.success(snapshot))
|
completion(.success(snapshot))
|
||||||
|
@ -148,7 +199,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
self.older = pagination?.older
|
self.older = pagination?.older
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
var snapshot = currentSnapshot
|
var snapshot = currentSnapshot()
|
||||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||||
snapshot.deleteItems([.confirmLoadMore])
|
snapshot.deleteItems([.confirmLoadMore])
|
||||||
completion(.success(snapshot))
|
completion(.success(snapshot))
|
||||||
|
@ -157,7 +208,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
guard let newer = newer else {
|
guard let newer = newer else {
|
||||||
completion(.failure(.noNewer))
|
completion(.failure(.noNewer))
|
||||||
return
|
return
|
||||||
|
@ -170,6 +221,11 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
completion(.failure(.client(error)))
|
completion(.failure(.client(error)))
|
||||||
|
|
||||||
case let .success(statuses, pagination):
|
case let .success(statuses, pagination):
|
||||||
|
guard !statuses.isEmpty else {
|
||||||
|
completion(.failure(.allCaughtUp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if there are no new statuses, pagination is nil
|
// if there are no new statuses, pagination is nil
|
||||||
// if we were to then overwrite self.newer, future refresh would fail
|
// if we were to then overwrite self.newer, future refresh would fail
|
||||||
if let newer = pagination?.newer {
|
if let newer = pagination?.newer {
|
||||||
|
@ -177,7 +233,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
var snapshot = currentSnapshot
|
var snapshot = currentSnapshot()
|
||||||
let newIdentifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
|
let newIdentifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
|
||||||
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
if let first = snapshot.itemIdentifiers(inSection: .statuses).first {
|
||||||
snapshot.insertItems(newIdentifiers, beforeItem: first)
|
snapshot.insertItems(newIdentifiers, beforeItem: first)
|
||||||
|
@ -196,16 +252,28 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||||
|
super.scrollViewWillBeginDragging(scrollView)
|
||||||
|
|
||||||
|
if isShowingTimelineDescription {
|
||||||
|
var snapshot = self.dataSource.snapshot()
|
||||||
|
snapshot.deleteSections([.header])
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineTableViewController {
|
extension TimelineTableViewController {
|
||||||
enum Section: Hashable, CaseIterable {
|
enum Section: Hashable, CaseIterable {
|
||||||
|
case header
|
||||||
case statuses
|
case statuses
|
||||||
case footer
|
case footer
|
||||||
}
|
}
|
||||||
enum Item: Hashable {
|
enum Item: Hashable {
|
||||||
case status(id: String, state: StatusState)
|
case status(id: String, state: StatusState)
|
||||||
case confirmLoadMore
|
case confirmLoadMore
|
||||||
|
case publicTimelineDescription(local: Bool)
|
||||||
|
|
||||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
|
@ -213,6 +281,8 @@ extension TimelineTableViewController {
|
||||||
return a == b
|
return a == b
|
||||||
case (.confirmLoadMore, .confirmLoadMore):
|
case (.confirmLoadMore, .confirmLoadMore):
|
||||||
return true
|
return true
|
||||||
|
case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
||||||
|
return a == b
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -225,6 +295,9 @@ extension TimelineTableViewController {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
case .confirmLoadMore:
|
case .confirmLoadMore:
|
||||||
hasher.combine(1)
|
hasher.combine(1)
|
||||||
|
case let .publicTimelineDescription(local: local):
|
||||||
|
hasher.combine(2)
|
||||||
|
hasher.combine(local)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
pruneOffscreenRows()
|
pruneOffscreenRows()
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
class func refreshCommandTitle() -> String {
|
class func refreshCommandTitle() -> String {
|
||||||
|
@ -111,13 +112,27 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
state = .loadingInitial
|
state = .loadingInitial
|
||||||
|
|
||||||
loadInitialItems() { result in
|
loadInitialItems() { result in
|
||||||
guard case let .success(snapshot) = result else {
|
|
||||||
self.state = .unloaded
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case let .success(snapshot):
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
self.state = .loaded
|
self.state = .loaded
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
self.state = .unloaded
|
||||||
|
var config = ToastConfiguration(title: "Error Loading")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadInitial()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
self.state = .unloaded
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,14 +147,28 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
|
|
||||||
state = .loadingOlder
|
state = .loadingOlder
|
||||||
|
|
||||||
loadOlderItems(currentSnapshot: dataSource.snapshot()) { result in
|
loadOlderItems(currentSnapshot: dataSource.snapshot) { result in
|
||||||
guard case let .success(snapshot) = result else {
|
|
||||||
self.state = .loaded
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
|
||||||
self.state = .loaded
|
self.state = .loaded
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case let .success(snapshot):
|
||||||
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
var config = ToastConfiguration(title: "Error Loading Older")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.loadOlder()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,8 +185,9 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
// this assumes that indexPathsForVisibleRows is always in order
|
// this assumes that indexPathsForVisibleRows is always in order
|
||||||
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||||
|
|
||||||
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.filter { timelineContentSections().contains($0) }
|
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.enumerated().filter { timelineContentSections().contains($0.element) }
|
||||||
if indexPath.section == orderedContentSections.count - 1,
|
if let lastContentSection = orderedContentSections.last,
|
||||||
|
indexPath.section == lastContentSection.offset,
|
||||||
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
||||||
|
|
||||||
loadOlder()
|
loadOlder()
|
||||||
|
@ -183,35 +213,58 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
|
|
||||||
state = .loadingNewer
|
state = .loadingNewer
|
||||||
|
|
||||||
let snapshot = dataSource.snapshot()
|
var firstItem: Item? = nil
|
||||||
|
let currentSnapshot: () -> Snapshot = {
|
||||||
|
let snapshot = self.dataSource.snapshot()
|
||||||
|
|
||||||
var item: Item? = nil
|
for section in self.timelineContentSections() {
|
||||||
for section in timelineContentSections() {
|
if snapshot.indexOfSection(section) != nil,
|
||||||
if let first = snapshot.itemIdentifiers(inSection: section).first {
|
let first = snapshot.itemIdentifiers(inSection: section).first {
|
||||||
item = first
|
firstItem = first
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNewerItems(currentSnapshot: snapshot) { result in
|
return snapshot
|
||||||
guard case let .success(snapshot) = result else {
|
}
|
||||||
|
|
||||||
|
loadNewerItems(currentSnapshot: currentSnapshot) { result in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.refreshControl?.endRefreshing()
|
self.refreshControl?.endRefreshing()
|
||||||
self.state = .loaded
|
self.state = .loaded
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
switch result {
|
||||||
self.refreshControl?.endRefreshing()
|
case let .success(snapshot):
|
||||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
self.state = .loaded
|
if let firstItem = firstItem,
|
||||||
|
let indexPath = self.dataSource.indexPath(for: firstItem) {
|
||||||
if let item = item,
|
|
||||||
let indexPath = self.dataSource.indexPath(for: item) {
|
|
||||||
// maintain the current position in the list (don't scroll to top)
|
// maintain the current position in the list (don't scroll to top)
|
||||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case let .failure(.client(error)):
|
||||||
|
var config = ToastConfiguration(title: "Error Loading Newer")
|
||||||
|
config.subtitle = error.localizedDescription
|
||||||
|
config.systemImageName = error.systemImageName
|
||||||
|
config.actionTitle = "Retry"
|
||||||
|
config.action = { [weak self] (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
self?.refresh()
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
case .failure(.allCaughtUp):
|
||||||
|
var config = ToastConfiguration(title: "You're all caught up")
|
||||||
|
config.edge = .top
|
||||||
|
config.dismissAutomaticallyAfter = 2
|
||||||
|
config.action = { (toast) in
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
self.showToast(configuration: config, animated: true)
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,11 +279,11 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
||||||
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
fatalError("loadOlderItesm(completion:) must be implemented by subclasses")
|
fatalError("loadOlderItesm(completion:) must be implemented by subclasses")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||||
fatalError("loadNewerItems(completion:) must be implemented by subclasses")
|
fatalError("loadNewerItems(completion:) must be implemented by subclasses")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,6 +311,7 @@ extension DiffableTimelineLikeTableViewController {
|
||||||
case noClient
|
case noClient
|
||||||
case noOlder
|
case noOlder
|
||||||
case noNewer
|
case noNewer
|
||||||
|
case allCaughtUp
|
||||||
case client(Client.Error)
|
case client(Client.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,5 +319,20 @@ extension DiffableTimelineLikeTableViewController {
|
||||||
extension DiffableTimelineLikeTableViewController: BackgroundableViewController {
|
extension DiffableTimelineLikeTableViewController: BackgroundableViewController {
|
||||||
func sceneDidEnterBackground() {
|
func sceneDidEnterBackground() {
|
||||||
pruneOffscreenRows()
|
pruneOffscreenRows()
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DiffableTimelineLikeTableViewController: ToastableViewController {
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension Client.Error {
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .networkError(_):
|
||||||
|
return "wifi.exclamationmark"
|
||||||
|
default:
|
||||||
|
return "exclamationmark.triangle"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,9 @@ extension MenuPreviewProvider {
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
case let .success(results, _) = response,
|
case let .success(results, _) = response,
|
||||||
let relationship = results.first else {
|
let relationship = results.first else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
elementHandler([])
|
elementHandler([])
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let following = relationship.following
|
let following = relationship.following
|
||||||
|
|
|
@ -143,9 +143,9 @@ class ContentTextView: LinkTextView {
|
||||||
case "del":
|
case "del":
|
||||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
case "code":
|
case "code":
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
case "pre":
|
case "pre":
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
case "ol", "ul":
|
case "ol", "ul":
|
||||||
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
@ -157,7 +157,7 @@ class ContentTextView: LinkTextView {
|
||||||
if parentTag == "ol" {
|
if parentTag == "ol" {
|
||||||
let index = (try? node.elementSiblingIndex()) ?? 0
|
let index = (try? node.elementSiblingIndex()) ?? 0
|
||||||
// we use the monospace digit font so that the periods of all the list items line up
|
// we use the monospace digit font so that the periods of all the list items line up
|
||||||
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: self.font!.pointSize, weight: .regular)])
|
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: defaultFont.pointSize, weight: .regular)])
|
||||||
} else if parentTag == "ul" {
|
} else if parentTag == "ul" {
|
||||||
bullet = NSAttributedString(string: "\u{2022}\t")
|
bullet = NSAttributedString(string: "\u{2022}\t")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
//
|
||||||
|
// PublicTimelineDescriptionTableViewCell.swift
|
||||||
|
// PublicTimelineDescriptionTableViewCell
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/7/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PublicTimelineDescriptionTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
var local = false {
|
||||||
|
didSet {
|
||||||
|
updateLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var didDismiss: (() -> Void)?
|
||||||
|
|
||||||
|
@IBOutlet private weak var descriptionLabel: UILabel!
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
contentView.backgroundColor = .tintColor
|
||||||
|
} else {
|
||||||
|
contentView.backgroundColor = .systemBlue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLabel() {
|
||||||
|
let str = NSMutableAttributedString()
|
||||||
|
let instanceStr = NSAttributedString(string: mastodonController.instanceURL.host!, attributes: [
|
||||||
|
.font: UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold)!, size: 0)
|
||||||
|
])
|
||||||
|
if local {
|
||||||
|
str.append(NSAttributedString(string: "The local timeline shows public posts from only "))
|
||||||
|
str.append(instanceStr)
|
||||||
|
str.append(NSAttributedString(string: "."))
|
||||||
|
} else {
|
||||||
|
str.append(NSAttributedString(string: "The federated timeline shows public posts from all users that "))
|
||||||
|
str.append(instanceStr)
|
||||||
|
str.append(NSAttributedString(string: " knows about."))
|
||||||
|
}
|
||||||
|
descriptionLabel.attributedText = str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PublicTimelineDescriptionTableViewCell: SelectableTableViewCell {
|
||||||
|
func didSelectCell() {
|
||||||
|
didDismiss?()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19134"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="78" id="KGk-i7-Jjw" customClass="PublicTimelineDescriptionTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="78"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="320" height="78"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="N97-CH-58I">
|
||||||
|
<rect key="frame" x="16" y="8" width="288" height="62"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="N97-CH-58I" secondAttribute="bottom" constant="8" id="2Lg-we-j2c"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="FdS-q9-obT"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="trailing" secondItem="H2p-sc-9uM" secondAttribute="trailingMargin" id="KqX-Qy-18G"/>
|
||||||
|
<constraint firstItem="N97-CH-58I" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="Pqy-8N-OnX"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="descriptionLabel" destination="N97-CH-58I" id="z1W-HD-xy9"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="131.8840579710145" y="121.875"/>
|
||||||
|
</tableViewCell>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<systemColor name="systemBlueColor">
|
||||||
|
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// ToastConfiguration.swift
|
||||||
|
// ToastConfiguration
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ToastConfiguration {
|
||||||
|
var systemImageName: String?
|
||||||
|
var titleFont: UIFont = .boldSystemFont(ofSize: 14)
|
||||||
|
var title: String
|
||||||
|
var subtitle: String?
|
||||||
|
var actionTitle: String?
|
||||||
|
var action: ((ToastView) -> Void)?
|
||||||
|
var edgeSpacing: CGFloat = 8
|
||||||
|
var edge: Edge = .automatic
|
||||||
|
var dismissOnScroll = true
|
||||||
|
var dismissAutomaticallyAfter: TimeInterval? = nil
|
||||||
|
|
||||||
|
init(title: String) {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Edge: Equatable {
|
||||||
|
case top
|
||||||
|
case bottom
|
||||||
|
/// Determines edge based on the current device. Bottom on iPhone, top on iPad/Mac.
|
||||||
|
case automatic
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,255 @@
|
||||||
|
//
|
||||||
|
// ToastView.swift
|
||||||
|
// ToastView
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ToastView: UIView {
|
||||||
|
|
||||||
|
let configuration: ToastConfiguration
|
||||||
|
|
||||||
|
private var shrinkAnimator: UIViewPropertyAnimator?
|
||||||
|
private var recognizedGesture = false
|
||||||
|
private var shouldDismissOnScroll = false
|
||||||
|
private(set) var shouldDismissAutomatically = true
|
||||||
|
|
||||||
|
private var offscreenTranslation: CGFloat {
|
||||||
|
var translation = bounds.height + configuration.edgeSpacing
|
||||||
|
if configuration.edge == .bottom {
|
||||||
|
translation += superview?.safeAreaInsets.bottom ?? 0
|
||||||
|
} else {
|
||||||
|
translation += superview?.safeAreaInsets.top ?? 0
|
||||||
|
translation *= -1
|
||||||
|
}
|
||||||
|
return translation
|
||||||
|
}
|
||||||
|
|
||||||
|
init(configuration: ToastConfiguration) {
|
||||||
|
precondition(configuration.edge != .automatic)
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupView() {
|
||||||
|
backgroundColor = .systemBlue
|
||||||
|
layer.shadowColor = UIColor.black.cgColor
|
||||||
|
layer.shadowRadius = 5
|
||||||
|
layer.shadowOffset = CGSize(width: 0, height: 2.5)
|
||||||
|
layer.shadowOpacity = 0.5
|
||||||
|
layer.masksToBounds = false
|
||||||
|
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.spacing = 8
|
||||||
|
|
||||||
|
if let name = configuration.systemImageName {
|
||||||
|
let imageView = UIImageView(image: UIImage(systemName: name))
|
||||||
|
imageView.tintColor = .white
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
stack.addArrangedSubview(imageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = configuration.title
|
||||||
|
titleLabel.textColor = .white
|
||||||
|
titleLabel.font = configuration.titleFont
|
||||||
|
titleLabel.adjustsFontSizeToFitWidth = true
|
||||||
|
|
||||||
|
if let subtitle = configuration.subtitle {
|
||||||
|
let subtitleLabel = UILabel()
|
||||||
|
subtitleLabel.text = subtitle
|
||||||
|
subtitleLabel.textColor = .white
|
||||||
|
subtitleLabel.numberOfLines = 0
|
||||||
|
subtitleLabel.font = .systemFont(ofSize: 14)
|
||||||
|
let vStack = UIStackView(arrangedSubviews: [
|
||||||
|
titleLabel,
|
||||||
|
subtitleLabel
|
||||||
|
])
|
||||||
|
vStack.axis = .vertical
|
||||||
|
vStack.spacing = 4
|
||||||
|
stack.addArrangedSubview(vStack)
|
||||||
|
} else {
|
||||||
|
stack.addArrangedSubview(titleLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let actionTitle = configuration.actionTitle {
|
||||||
|
let actionLabel = UILabel()
|
||||||
|
actionLabel.text = actionTitle
|
||||||
|
actionLabel.font = .boldSystemFont(ofSize: 16)
|
||||||
|
actionLabel.textColor = .white
|
||||||
|
stack.addArrangedSubview(actionLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stack.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 1),
|
||||||
|
trailingAnchor.constraint(equalToSystemSpacingAfter: stack.trailingAnchor, multiplier: 1),
|
||||||
|
stack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
||||||
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
|
||||||
|
])
|
||||||
|
|
||||||
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
|
||||||
|
addGestureRecognizer(pan)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
layer.cornerRadius = min(32, bounds.height / 2)
|
||||||
|
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: layer.cornerRadius, cornerHeight: layer.cornerRadius, transform: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissToast(animated: Bool) {
|
||||||
|
guard animated else {
|
||||||
|
removeFromSuperview()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
|
} completion: { (_) in
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateAppearance() {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: offscreenTranslation)
|
||||||
|
let duration = 0.5
|
||||||
|
let velocity = 0.5
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: []) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDismissOnScroll(connectedTo scrollView: UIScrollView) {
|
||||||
|
guard configuration.dismissOnScroll else { return }
|
||||||
|
scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
recognizedGesture = false
|
||||||
|
shouldDismissAutomatically = false
|
||||||
|
|
||||||
|
shrinkAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut) {
|
||||||
|
self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
|
||||||
|
}
|
||||||
|
shrinkAnimator?.startAnimation(afterDelay: 0.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
|
super.touchesEnded(touches, with: event)
|
||||||
|
|
||||||
|
if !recognizedGesture {
|
||||||
|
guard let shrinkAnimator = shrinkAnimator else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shrinkAnimator.stopAnimation(true)
|
||||||
|
UIView.animate(withDuration: 0.1, delay: 0, options: .curveEaseInOut) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
configuration.action?(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
shrinkAnimator = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
let translation = recognizer.translation(in: self).y
|
||||||
|
|
||||||
|
let isDraggingAwayFromDismissalEdge = (configuration.edge == .top && translation > 0) || (configuration.edge == .bottom && translation < 0)
|
||||||
|
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
recognizedGesture = true
|
||||||
|
shouldDismissAutomatically = false
|
||||||
|
UIView.animate(withDuration: 0.1, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
var distance: CGFloat
|
||||||
|
if isDraggingAwayFromDismissalEdge {
|
||||||
|
distance = sqrt(abs(translation))
|
||||||
|
if configuration.edge == .bottom {
|
||||||
|
distance *= -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
distance = translation
|
||||||
|
}
|
||||||
|
transform = CGAffineTransform(translationX: 0, y: distance)
|
||||||
|
|
||||||
|
case .ended, .cancelled:
|
||||||
|
let velocity = recognizer.velocity(in: self).y
|
||||||
|
let distance = isDraggingAwayFromDismissalEdge ? sqrt(abs(translation)) : translation
|
||||||
|
|
||||||
|
let minDismissalDistance = configuration.edgeSpacing + bounds.height / 2
|
||||||
|
let dismissDueToDistance = configuration.edge == .bottom ? distance > minDismissalDistance : -distance > minDismissalDistance
|
||||||
|
|
||||||
|
let minDismissalVelocity: CGFloat = 250
|
||||||
|
let dismissDueToVelocity = configuration.edge == .bottom ? velocity > minDismissalDistance : velocity < -minDismissalVelocity
|
||||||
|
if dismissDueToDistance || dismissDueToVelocity {
|
||||||
|
|
||||||
|
if abs(translation) < abs(offscreenTranslation) {
|
||||||
|
let distanceLeft = offscreenTranslation - translation
|
||||||
|
let duration = 1 / TimeInterval(max(velocity, minDismissalVelocity) / distanceLeft)
|
||||||
|
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction) {
|
||||||
|
self.transform = CGAffineTransform(translationX: 0, y: self.offscreenTranslation)
|
||||||
|
} completion: { (_) in
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let duration = 0.5
|
||||||
|
let velocity = distance * duration
|
||||||
|
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: velocity, options: .allowUserInteraction) {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
shouldDismissOnScroll = true
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let translation = recognizer.translation(in: recognizer.view).y
|
||||||
|
if shouldDismissOnScroll && abs(translation) > 50 {
|
||||||
|
dismissToast(animated: true)
|
||||||
|
shouldDismissOnScroll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .ended, .cancelled:
|
||||||
|
shouldDismissOnScroll = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// ToastableViewController.swift
|
||||||
|
// ToastableViewController
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/14/21.
|
||||||
|
// Copyright © 2021 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ToastableViewController: UIViewController {
|
||||||
|
|
||||||
|
var toastParentView: UIView { get }
|
||||||
|
var toastScrollView: UIScrollView? { get }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentToastKey = "Tusker_currentToast"
|
||||||
|
|
||||||
|
extension ToastableViewController {
|
||||||
|
|
||||||
|
private(set) var currentToast: ToastView? {
|
||||||
|
get {
|
||||||
|
let holder = objc_getAssociatedObject(self, ¤tToastKey) as? WeakHolder<ToastView>
|
||||||
|
return holder?.object
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let newValue = newValue {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, WeakHolder(object: newValue), .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
} else {
|
||||||
|
objc_setAssociatedObject(self, ¤tToastKey, nil, .OBJC_ASSOCIATION_RETAIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toastParentView: UIView { view }
|
||||||
|
var toastScrollView: UIScrollView? { view as? UIScrollView }
|
||||||
|
|
||||||
|
func showToast(configuration config: ToastConfiguration, animated: Bool) {
|
||||||
|
currentToast?.dismissToast(animated: false)
|
||||||
|
|
||||||
|
var config = config
|
||||||
|
config.edge = effectiveEdge(edge: config.edge)
|
||||||
|
|
||||||
|
let toast = ToastView(configuration: config)
|
||||||
|
currentToast = toast
|
||||||
|
|
||||||
|
let parentSafeArea = toastParentView.safeAreaLayoutGuide
|
||||||
|
toast.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
toastParentView.addSubview(toast)
|
||||||
|
|
||||||
|
let yConstraint: NSLayoutConstraint
|
||||||
|
switch config.edge {
|
||||||
|
case .top:
|
||||||
|
yConstraint = toast.topAnchor.constraint(equalTo: parentSafeArea.topAnchor, constant: config.edgeSpacing)
|
||||||
|
case .bottom:
|
||||||
|
yConstraint = parentSafeArea.bottomAnchor.constraint(equalTo: toast.bottomAnchor, constant: config.edgeSpacing)
|
||||||
|
case .automatic:
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
toast.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: parentSafeArea.leadingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: toast.trailingAnchor, multiplier: 1),
|
||||||
|
parentSafeArea.centerXAnchor.constraint(equalTo: toast.centerXAnchor),
|
||||||
|
yConstraint,
|
||||||
|
])
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
toast.animateAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.dismissOnScroll,
|
||||||
|
let scrollView = toastScrollView {
|
||||||
|
toast.setupDismissOnScroll(connectedTo: scrollView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let time = config.dismissAutomaticallyAfter {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + time) { [weak toast] in
|
||||||
|
guard let toast = toast, toast.shouldDismissAutomatically else { return }
|
||||||
|
toast.dismissToast(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func effectiveEdge(edge: ToastConfiguration.Edge) -> ToastConfiguration.Edge {
|
||||||
|
guard case .automatic = edge else {
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
return .bottom
|
||||||
|
} else {
|
||||||
|
return .top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate class WeakHolder<T: AnyObject> {
|
||||||
|
weak var object: T?
|
||||||
|
|
||||||
|
init(object: T) {
|
||||||
|
self.object = object
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue