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
|
||||
|
||||
## 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)
|
||||
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>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||
<string>Action</string>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
|
||||
|
@ -35,6 +33,8 @@
|
|||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>NSExtensionJavaScriptPreprocessingFile</key>
|
||||
<string>Action</string>
|
||||
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
|
||||
<true/>
|
||||
<key>NSExtensionServiceAllowsTouchBarItem</key>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public class NotificationGroup {
|
||||
public class NotificationGroup: Identifiable, Hashable {
|
||||
public let notifications: [Notification]
|
||||
public let id: String
|
||||
public let kind: Notification.Kind
|
||||
|
@ -25,6 +25,14 @@ public class NotificationGroup {
|
|||
self.statusState = nil
|
||||
}
|
||||
}
|
||||
|
||||
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] {
|
||||
var groups = [[Notification]]()
|
||||
|
@ -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 */; };
|
||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.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 */; };
|
||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.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 */; };
|
||||
D64BC18823C1640A000D0238 /* PinStatusActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BC18723C1640A000D0238 /* PinStatusActivity.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1058,27 +1068,27 @@
|
|||
D641C780213DD7C4004B4513 /* Screens */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||
D641C782213DD7F0004B4513 /* Main */,
|
||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||
D641C781213DD7DD004B4513 /* Timeline */,
|
||||
D641C784213DD819004B4513 /* Profile */,
|
||||
D641C785213DD83B004B4513 /* Conversation */,
|
||||
D641C786213DD852004B4513 /* Notifications */,
|
||||
D641C787213DD862004B4513 /* Compose */,
|
||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||
D6B053A023BD2BED00A066FA /* Asset Picker */,
|
||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||
D641C787213DD862004B4513 /* Compose */,
|
||||
D641C785213DD83B004B4513 /* Conversation */,
|
||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||
D627FF77217E94F200CC0648 /* Drafts */,
|
||||
D627943C23A5635D00D38C68 /* Explore */,
|
||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||
D627944B23A9A02400D38C68 /* Lists */,
|
||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||
D641C788213DD86D004B4513 /* Large Image */,
|
||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||
D627944823A6AD5100D38C68 /* Bookmarks */,
|
||||
D627944B23A9A02400D38C68 /* Lists */,
|
||||
D641C782213DD7F0004B4513 /* Main */,
|
||||
D641C786213DD852004B4513 /* Notifications */,
|
||||
D641C783213DD7FE004B4513 /* Onboarding */,
|
||||
D641C789213DD87E004B4513 /* Preferences */,
|
||||
D641C784213DD819004B4513 /* Profile */,
|
||||
D6BC9DD8232D8BCA002CA326 /* Search */,
|
||||
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
|
||||
D641C781213DD7DD004B4513 /* Timeline */,
|
||||
D6C693FA2162FE5D007D6A6D /* Utilities */,
|
||||
);
|
||||
path = Screens;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1240,6 +1250,15 @@
|
|||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6420AEB26BED17500ED8175 /* Timeline Description Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6420AEC26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift */,
|
||||
D6420AED26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib */,
|
||||
);
|
||||
path = "Timeline Description Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D646C954213B364600269FB5 /* Transitions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1250,6 +1269,16 @@
|
|||
path = Transitions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */,
|
||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */,
|
||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */,
|
||||
);
|
||||
path = Toast;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D65A37F221472F300087646E /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1441,35 +1470,37 @@
|
|||
D6BED1722126661300F02DA0 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
|
||||
D620483523D38075008A63EF /* ContentTextView.swift */,
|
||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
|
||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||
D620483323D3801D008A63EF /* LinkTextView.swift */,
|
||||
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
|
||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D620483723D38190008A63EF /* StatusContentTextView.swift */,
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
|
||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
|
||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
|
||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||
D626494023C122C800612E6E /* Asset Picker */,
|
||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
D641C78A213DD926004B4513 /* Status */,
|
||||
D6C7D27B22B6EBE200071952 /* Attachments */,
|
||||
D623A53B2635F4E20095BD04 /* Poll */,
|
||||
D641C78B213DD92F004B4513 /* Profile Header */,
|
||||
D641C78C213DD937004B4513 /* Notifications */,
|
||||
D6A3BC872321F78000FD64D5 /* Account Cell */,
|
||||
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
|
||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||
D611C2CC232DC5FC00C86A49 /* Hashtag 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;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1477,24 +1508,24 @@
|
|||
D6C693FA2162FE5D007D6A6D /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
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 */,
|
||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
|
||||
D65234C8256189D0001AF9CF /* TimelineLikeTableViewController.swift */,
|
||||
D653F410267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.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 */,
|
||||
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;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1541,36 +1572,36 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
|
||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
|
||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
|
||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
|
||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.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 */,
|
||||
D6AEBB3F2321640F00E5038B /* Activities */,
|
||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||
D6F953F121251A2F00CF0F2B /* Controllers */,
|
||||
D6370B9924421FE00092A7FF /* CoreData */,
|
||||
D667E5F62135C2ED0057A976 /* Extensions */,
|
||||
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
|
||||
D6D4DDDB212518A200E1C4BB /* Info.plist */,
|
||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
|
||||
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
|
||||
D61959D2241E846D00A37B8E /* Models */,
|
||||
D663626021360A9600C9CBA2 /* Preferences */,
|
||||
D641C780213DD7C4004B4513 /* Screens */,
|
||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||
D6BED1722126661300F02DA0 /* Views */,
|
||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||
);
|
||||
path = Tusker;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1886,6 +1917,7 @@
|
|||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||
D6420AEF26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.xib in Resources */,
|
||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||
|
@ -2052,6 +2084,7 @@
|
|||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||
|
@ -2133,6 +2166,7 @@
|
|||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */,
|
||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||
|
@ -2147,6 +2181,8 @@
|
|||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
|
||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */,
|
||||
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
|
||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||
|
@ -2509,6 +2545,7 @@
|
|||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -2565,6 +2602,7 @@
|
|||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
|
@ -2577,7 +2615,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
@ -2610,7 +2648,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
@ -2719,7 +2757,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
@ -2746,7 +2784,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
|
|
|
@ -67,6 +67,9 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
||||
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 {
|
||||
|
@ -102,6 +105,9 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
try container.encode(silentActions, forKey: .silentActions)
|
||||
try container.encode(statusContentType, forKey: .statusContentType)
|
||||
|
||||
try container.encode(hasShownLocalTimelineDescription, forKey: .hasShownLocalTimelineDescription)
|
||||
try container.encode(hasShownFederatedTimelineDescription, forKey: .hasShownFederatedTimelineDescription)
|
||||
}
|
||||
|
||||
// MARK: Appearance
|
||||
|
@ -140,7 +146,11 @@ class Preferences: Codable, ObservableObject {
|
|||
// MARK: Advanced
|
||||
@Published var silentActions: [String: Permission] = [:]
|
||||
@Published var statusContentType: StatusContentType = .plain
|
||||
|
||||
|
||||
// MARK:
|
||||
@Published var hasShownLocalTimelineDescription = false
|
||||
@Published var hasShownFederatedTimelineDescription = false
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case theme
|
||||
case avatarStyle
|
||||
|
@ -172,6 +182,9 @@ class Preferences: Codable, ObservableObject {
|
|||
|
||||
case silentActions
|
||||
case statusContentType
|
||||
|
||||
case hasShownLocalTimelineDescription
|
||||
case hasShownFederatedTimelineDescription
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -73,12 +73,12 @@ class FastAccountSwitcherViewController: UIViewController {
|
|||
|
||||
accountView.alpha = 0
|
||||
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.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,3 +89,11 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
|||
root.performSearch(query: query)
|
||||
}
|
||||
}
|
||||
|
||||
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
if let backgroundable = root as? BackgroundableViewController {
|
||||
backgroundable.sceneDidEnterBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
|
||||
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> {
|
||||
|
||||
private let statusCell = "statusCell"
|
||||
private let actionGroupCell = "actionGroupCell"
|
||||
|
@ -54,88 +54,9 @@ class NotificationsTableViewController: TimelineLikeTableViewController<Notifica
|
|||
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
|
||||
}
|
||||
|
||||
override func loadInitialItems(completion: @escaping ([NotificationGroup]) -> Void) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// MARK: - DiffableTimelineLikeTableViewController
|
||||
|
||||
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 {
|
||||
case .mention:
|
||||
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
|
||||
|
||||
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 {
|
||||
var apiController: MastodonController { mastodonController }
|
||||
}
|
||||
|
@ -224,7 +257,8 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
|
|||
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +266,8 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
|
|||
|
||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
var urlHandler: AnyCancellable?
|
||||
var currentQuery: String?
|
||||
|
||||
private var activityIndicator: UIActivityIndicatorView!
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return .portrait
|
||||
|
@ -49,11 +51,16 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// 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.estimatedRowHeight = 120
|
||||
createActivityIndicatorHeader()
|
||||
|
||||
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
|
@ -73,12 +80,19 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.searchBar.searchTextField.autocapitalizationType = .none
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
definesPresentationContext = true
|
||||
|
||||
urlHandler = urlCheckerSubject
|
||||
.debounce(for: .seconds(1), scheduler: RunLoop.main)
|
||||
.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()
|
||||
}
|
||||
|
@ -112,6 +126,8 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
}
|
||||
|
||||
private func updateSpecificInstance(domain: String) {
|
||||
activityIndicator.startAnimating()
|
||||
|
||||
let components = parseURLComponents(input: domain)
|
||||
let url = components.url!
|
||||
|
||||
|
@ -120,16 +136,26 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
client.run(request) { (response) in
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
if snapshot.indexOfSection(.selected) != nil {
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
|
||||
snapshot.deleteSections([.selected])
|
||||
}
|
||||
|
||||
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.appendItems([.selected(url, instance)], toSection: .selected)
|
||||
|
||||
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() {
|
||||
InstanceSelector.getInstances(category: nil) { (response) in
|
||||
guard case let .success(instances, _) = response else { fatalError() }
|
||||
|
||||
self.recommendedInstances = instances
|
||||
self.filterRecommendedResults()
|
||||
DispatchQueue.main.async {
|
||||
switch response {
|
||||
case let .failure(error):
|
||||
self.showRecommendationsError(error)
|
||||
case let .success(instances, _):
|
||||
self.recommendedInstances = instances
|
||||
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]
|
||||
if let currentQuery = currentQuery, !currentQuery.isEmpty {
|
||||
filteredInstances = recommendedInstances.filter {
|
||||
|
@ -155,12 +236,20 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
}
|
||||
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteSections([.recommendedInstances])
|
||||
snapshot.appendSections([.recommendedInstances])
|
||||
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot)
|
||||
if snapshot.indexOfSection(.recommendedInstances) != nil {
|
||||
let toRemove = snapshot.itemIdentifiers(inSection: .recommendedInstances).filter {
|
||||
if case .recommended(_) = $0 {
|
||||
return true
|
||||
} 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
|
||||
|
@ -194,29 +283,30 @@ extension InstanceSelectorTableViewController {
|
|||
case recommended(InstanceSelector.Instance)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
if case let .selected(url, instance) = lhs,
|
||||
case let .selected(otherUrl, other) = rhs {
|
||||
return url == otherUrl && instance.uri == other.uri
|
||||
} else if case let .recommended(instance) = lhs,
|
||||
case let .recommended(other) = rhs {
|
||||
return instance.domain == other.domain
|
||||
switch (lhs, rhs) {
|
||||
case let (.selected(urlA, instanceA), .selected(urlB, instanceB)):
|
||||
return urlA == urlB && instanceA.uri == instanceB.uri
|
||||
case let (.recommended(a), .recommended(b)):
|
||||
return a.domain == b.domain
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .selected(url, instance):
|
||||
hasher.combine(Section.selected)
|
||||
hasher.combine(0)
|
||||
hasher.combine(url)
|
||||
hasher.combine(instance.uri)
|
||||
case let .recommended(instance):
|
||||
hasher.combine(Section.recommendedInstances)
|
||||
hasher.combine(1)
|
||||
hasher.combine(instance.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,8 +70,8 @@ class ProfileViewController: UIPageViewController {
|
|||
|
||||
let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
|
||||
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
|
||||
self.composeDirectMentioning()
|
||||
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { [weak self] (_) in
|
||||
self?.composeDirectMentioning()
|
||||
})
|
||||
])
|
||||
composeButton.isEnabled = mastodonController.loggedIn
|
||||
|
|
|
@ -20,6 +20,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
private var older: RequestRange?
|
||||
|
||||
private var didConfirmLoadMore = false
|
||||
private var isShowingTimelineDescription = false
|
||||
|
||||
init(for timeline: Timeline, mastodonController: MastodonController) {
|
||||
self.timeline = timeline
|
||||
|
@ -58,6 +59,42 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
||||
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
|
||||
|
@ -87,6 +124,17 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
self.didConfirmLoadMore = false
|
||||
}
|
||||
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,16 +156,19 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
self.older = pagination?.older
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
var snapshot = Snapshot()
|
||||
snapshot.appendSections([.statuses, .footer])
|
||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||
completion(.success(snapshot))
|
||||
DispatchQueue.main.async {
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteSections([.statuses, .footer])
|
||||
snapshot.appendSections([.statuses, .footer])
|
||||
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
|
||||
completion(.success(snapshot))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
|
||||
guard let older = older else {
|
||||
completion(.failure(.noOlder))
|
||||
return
|
||||
|
@ -125,12 +176,12 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
|
||||
if #available(iOS 15.0, *),
|
||||
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"
|
||||
completion(.success(currentSnapshot))
|
||||
completion(.success(snapshot))
|
||||
return
|
||||
}
|
||||
var snapshot = currentSnapshot
|
||||
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
|
||||
self.dataSource.apply(snapshot)
|
||||
completion(.success(snapshot))
|
||||
|
@ -148,7 +199,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
self.older = pagination?.older
|
||||
|
||||
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.deleteItems([.confirmLoadMore])
|
||||
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 {
|
||||
completion(.failure(.noNewer))
|
||||
return
|
||||
|
@ -170,6 +221,11 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
completion(.failure(.client(error)))
|
||||
|
||||
case let .success(statuses, pagination):
|
||||
guard !statuses.isEmpty else {
|
||||
completion(.failure(.allCaughtUp))
|
||||
return
|
||||
}
|
||||
|
||||
// if there are no new statuses, pagination is nil
|
||||
// if we were to then overwrite self.newer, future refresh would fail
|
||||
if let newer = pagination?.newer {
|
||||
|
@ -177,7 +233,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
|
|||
}
|
||||
|
||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||
var snapshot = currentSnapshot
|
||||
var snapshot = currentSnapshot()
|
||||
let newIdentifiers = statuses.map { Item.status(id: $0.id, state: .unknown) }
|
||||
if let first = snapshot.itemIdentifiers(inSection: .statuses).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 {
|
||||
enum Section: Hashable, CaseIterable {
|
||||
case header
|
||||
case statuses
|
||||
case footer
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: StatusState)
|
||||
case confirmLoadMore
|
||||
case publicTimelineDescription(local: Bool)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
|
@ -213,6 +281,8 @@ extension TimelineTableViewController {
|
|||
return a == b
|
||||
case (.confirmLoadMore, .confirmLoadMore):
|
||||
return true
|
||||
case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
|
||||
return a == b
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -225,6 +295,9 @@ extension TimelineTableViewController {
|
|||
hasher.combine(id)
|
||||
case .confirmLoadMore:
|
||||
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)
|
||||
|
||||
pruneOffscreenRows()
|
||||
currentToast?.dismissToast(animated: false)
|
||||
}
|
||||
|
||||
class func refreshCommandTitle() -> String {
|
||||
|
@ -111,13 +112,27 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
|||
state = .loadingInitial
|
||||
|
||||
loadInitialItems() { result in
|
||||
guard case let .success(snapshot) = result else {
|
||||
self.state = .unloaded
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
self.state = .loaded
|
||||
switch result {
|
||||
case let .success(snapshot):
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
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
|
||||
|
||||
loadOlderItems(currentSnapshot: dataSource.snapshot()) { result in
|
||||
guard case let .success(snapshot) = result else {
|
||||
self.state = .loaded
|
||||
return
|
||||
}
|
||||
loadOlderItems(currentSnapshot: dataSource.snapshot) { result in
|
||||
DispatchQueue.main.async {
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
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
|
||||
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
|
||||
|
||||
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.filter { timelineContentSections().contains($0) }
|
||||
if indexPath.section == orderedContentSections.count - 1,
|
||||
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.enumerated().filter { timelineContentSections().contains($0.element) }
|
||||
if let lastContentSection = orderedContentSections.last,
|
||||
indexPath.section == lastContentSection.offset,
|
||||
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
|
||||
|
||||
loadOlder()
|
||||
|
@ -183,34 +213,57 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
|
|||
|
||||
state = .loadingNewer
|
||||
|
||||
let snapshot = dataSource.snapshot()
|
||||
|
||||
var item: Item? = nil
|
||||
for section in timelineContentSections() {
|
||||
if let first = snapshot.itemIdentifiers(inSection: section).first {
|
||||
item = first
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
loadNewerItems(currentSnapshot: snapshot) { result in
|
||||
guard case let .success(snapshot) = result else {
|
||||
DispatchQueue.main.async {
|
||||
self.refreshControl?.endRefreshing()
|
||||
self.state = .loaded
|
||||
var firstItem: Item? = nil
|
||||
let currentSnapshot: () -> Snapshot = {
|
||||
let snapshot = self.dataSource.snapshot()
|
||||
|
||||
for section in self.timelineContentSections() {
|
||||
if snapshot.indexOfSection(section) != nil,
|
||||
let first = snapshot.itemIdentifiers(inSection: section).first {
|
||||
firstItem = first
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
loadNewerItems(currentSnapshot: currentSnapshot) { result in
|
||||
DispatchQueue.main.async {
|
||||
self.refreshControl?.endRefreshing()
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
self.state = .loaded
|
||||
|
||||
if let item = item,
|
||||
let indexPath = self.dataSource.indexPath(for: item) {
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false)
|
||||
|
||||
switch result {
|
||||
case let .success(snapshot):
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
if let firstItem = firstItem,
|
||||
let indexPath = self.dataSource.indexPath(for: firstItem) {
|
||||
// maintain the current position in the list (don't scroll to top)
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -258,6 +311,7 @@ extension DiffableTimelineLikeTableViewController {
|
|||
case noClient
|
||||
case noOlder
|
||||
case noNewer
|
||||
case allCaughtUp
|
||||
case client(Client.Error)
|
||||
}
|
||||
}
|
||||
|
@ -265,5 +319,20 @@ extension DiffableTimelineLikeTableViewController {
|
|||
extension DiffableTimelineLikeTableViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
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,
|
||||
case let .success(results, _) = response,
|
||||
let relationship = results.first else {
|
||||
elementHandler([])
|
||||
DispatchQueue.main.async {
|
||||
elementHandler([])
|
||||
}
|
||||
return
|
||||
}
|
||||
let following = relationship.following
|
||||
|
|
|
@ -143,9 +143,9 @@ class ContentTextView: LinkTextView {
|
|||
case "del":
|
||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||
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":
|
||||
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"))
|
||||
case "ol", "ul":
|
||||
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||
|
@ -157,7 +157,7 @@ class ContentTextView: LinkTextView {
|
|||
if parentTag == "ol" {
|
||||
let index = (try? node.elementSiblingIndex()) ?? 0
|
||||
// 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" {
|
||||
bullet = NSAttributedString(string: "\u{2022}\t")
|
||||
} 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