Compare commits

..

No commits in common. "aced0a63c95714fb82185fa9d57ad2391b980245" and "6d8a014cc7d13a3362b7a0c30c009553a45bbec3" have entirely different histories.

19 changed files with 245 additions and 1091 deletions

View File

@ -1,19 +1,5 @@
# 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.

View File

@ -24,6 +24,8 @@
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsAttachmentsWithMinCount</key>
@ -33,8 +35,6 @@
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>Action</string>
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
<true/>
<key>NSExtensionServiceAllowsTouchBarItem</key>

View File

@ -8,7 +8,7 @@
import Foundation
public class NotificationGroup: Identifiable, Hashable {
public class NotificationGroup {
public let notifications: [Notification]
public let id: String
public let kind: Notification.Kind
@ -26,14 +26,6 @@ public class NotificationGroup: Identifiable, Hashable {
}
}
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]]()
for notification in notifications {
@ -58,3 +50,5 @@ public class NotificationGroup: Identifiable, Hashable {
}
}
extension NotificationGroup: Identifiable {}

View File

@ -134,16 +134,11 @@
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 */; };
@ -540,16 +535,11 @@
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>"; };
@ -1068,27 +1058,27 @@
D641C780213DD7C4004B4513 /* Screens */ = {
isa = PBXGroup;
children = (
D6A3BC822321F69400FD64D5 /* Account List */,
D6B053A023BD2BED00A066FA /* Asset Picker */,
0411610522B457290030A9B7 /* Attachment Gallery */,
D627944823A6AD5100D38C68 /* Bookmarks */,
D641C787213DD862004B4513 /* Compose */,
D641C785213DD83B004B4513 /* Conversation */,
D6C693FA2162FE5D007D6A6D /* Utilities */,
D6F2E960249E772F005846BB /* Crash Reporter */,
D641C782213DD7F0004B4513 /* Main */,
D641C783213DD7FE004B4513 /* Onboarding */,
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
D641C781213DD7DD004B4513 /* Timeline */,
D641C784213DD819004B4513 /* Profile */,
D641C785213DD83B004B4513 /* Conversation */,
D641C786213DD852004B4513 /* Notifications */,
D641C787213DD862004B4513 /* Compose */,
D6B053A023BD2BED00A066FA /* Asset Picker */,
D627FF77217E94F200CC0648 /* Drafts */,
D627943C23A5635D00D38C68 /* Explore */,
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
D641C788213DD86D004B4513 /* Large Image */,
D627944B23A9A02400D38C68 /* Lists */,
D641C782213DD7F0004B4513 /* Main */,
D641C786213DD852004B4513 /* Notifications */,
D641C783213DD7FE004B4513 /* Onboarding */,
D641C789213DD87E004B4513 /* Preferences */,
D641C784213DD819004B4513 /* Profile */,
D6BC9DD8232D8BCA002CA326 /* Search */,
D627944B23A9A02400D38C68 /* Lists */,
D641C788213DD86D004B4513 /* Large Image */,
0411610522B457290030A9B7 /* Attachment Gallery */,
D6A3BC822321F69400FD64D5 /* Account List */,
D6A3BC8C2321FF9B00FD64D5 /* Status Action Account List */,
D641C781213DD7DD004B4513 /* Timeline */,
D6C693FA2162FE5D007D6A6D /* Utilities */,
D627944823A6AD5100D38C68 /* Bookmarks */,
D641C789213DD87E004B4513 /* Preferences */,
);
path = Screens;
sourceTree = "<group>";
@ -1250,15 +1240,6 @@
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 = (
@ -1269,16 +1250,6 @@
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 = (
@ -1470,37 +1441,35 @@
D6BED1722126661300F02DA0 /* Views */ = {
isa = PBXGroup;
children = (
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
D620483323D3801D008A63EF /* LinkTextView.swift */,
D620483523D38075008A63EF /* ContentTextView.swift */,
D620483723D38190008A63EF /* StatusContentTextView.swift */,
D68E6F5E253C9B2D001A1B4C /* BaseEmojiLabel.swift */,
D6969E9F240C8384002843CE /* EmojiLabel.swift */,
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */,
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
D620483723D38190008A63EF /* StatusContentTextView.swift */,
04ED00B021481ED800567C53 /* SteppedProgressView.swift */,
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */,
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D6A3BC872321F78000FD64D5 /* Account Cell */,
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */,
D67C57A721E2649B00C3118B /* Account Detail */,
D626494023C122C800612E6E /* Asset Picker */,
D6C7D27B22B6EBE200071952 /* Attachments */,
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
D61959D0241E842400A37B8E /* Draft Cell */,
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
D61AC1DA232EA43100C54D2D /* Instance Cell */,
D641C78C213DD937004B4513 /* Notifications */,
D641C78A213DD926004B4513 /* Status */,
D6C7D27B22B6EBE200071952 /* Attachments */,
D623A53B2635F4E20095BD04 /* Poll */,
D641C78B213DD92F004B4513 /* Profile Header */,
D641C78A213DD926004B4513 /* Status */,
D64AAE8F26C80DB600FC57FB /* Toast */,
D6420AEB26BED17500ED8175 /* Timeline Description Cell */,
D641C78C213DD937004B4513 /* Notifications */,
D6A3BC872321F78000FD64D5 /* Account Cell */,
D611C2CC232DC5FC00C86A49 /* Hashtag Cell */,
D61AC1DA232EA43100C54D2D /* Instance Cell */,
D6DEA0DB268400AF00FE896A /* Confirm Load More Cell */,
);
path = Views;
sourceTree = "<group>";
@ -1508,24 +1477,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>";
@ -1572,36 +1541,36 @@
isa = PBXGroup;
children = (
D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
D6D4DDDB212518A200E1C4BB /* Info.plist */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D6AC956623C4347E008C9946 /* MainSceneDelegate.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 */,
D6D4DDD6212518A200E1C4BB /* Assets.xcassets */,
D6AEBB3F2321640F00E5038B /* Activities */,
D6F1F84E2193B9BE00F5FE67 /* Caching */,
D6F953F121251A2F00CF0F2B /* Controllers */,
D6370B9924421FE00092A7FF /* CoreData */,
D667E5F62135C2ED0057A976 /* Extensions */,
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
D6E57FA525C26FAB00341037 /* Localizable.stringsdict */,
D61959D2241E846D00A37B8E /* Models */,
D663626021360A9600C9CBA2 /* Preferences */,
D641C780213DD7C4004B4513 /* Screens */,
D62D241E217AA46B005076CC /* Shortcuts */,
D67B506B250B28FF00FAECFB /* Vendor */,
D6BED1722126661300F02DA0 /* Views */,
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 */,
D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */,
D6D4DDDB212518A200E1C4BB /* Info.plist */,
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */,
);
path = Tusker;
sourceTree = "<group>";
@ -1917,7 +1886,6 @@
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 */,
@ -2084,7 +2052,6 @@
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 */,
@ -2166,7 +2133,6 @@
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 */,
@ -2181,8 +2147,6 @@
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 */,
@ -2545,7 +2509,6 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
};
name = Debug;
};
@ -2602,7 +2565,6 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
VALIDATE_PRODUCT = YES;
};
name = Release;
@ -2615,7 +2577,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2648,7 +2610,7 @@
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = Tusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2757,7 +2719,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
@ -2784,7 +2746,7 @@
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = V4WK9KR9U2;
INFOPLIST_FILE = OpenInTusker/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;

View File

@ -67,9 +67,6 @@ 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 {
@ -105,9 +102,6 @@ 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
@ -147,10 +141,6 @@ class Preferences: Codable, ObservableObject {
@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
@ -182,9 +172,6 @@ class Preferences: Codable, ObservableObject {
case silentActions
case statusContentType
case hasShownLocalTimelineDescription
case hasShownFederatedTimelineDescription
}
}

View File

@ -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 / 2) {
UIView.addKeyframe(withRelativeStartTime: relStart, relativeDuration: relDuration) {
accountView.alpha = 1
accountView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
}
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration / 2, relativeDuration: relDuration / 2) {
UIView.addKeyframe(withRelativeStartTime: relStart + relDuration, relativeDuration: relDuration) {
accountView.transform = .identity
}
}

View File

@ -89,11 +89,3 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
root.performSearch(query: query)
}
}
extension AccountSwitchingContainerViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
if let backgroundable = root as? BackgroundableViewController {
backgroundable.sceneDidEnterBackground()
}
}
}

View File

@ -9,7 +9,7 @@
import UIKit
import Pachyderm
class NotificationsTableViewController: DiffableTimelineLikeTableViewController<NotificationsTableViewController.Section, NotificationGroup> {
class NotificationsTableViewController: TimelineLikeTableViewController<NotificationGroup> {
private let statusCell = "statusCell"
private let actionGroupCell = "actionGroupCell"
@ -54,9 +54,88 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
tableView.register(UINib(nibName: "BasicTableViewCell", bundle: .main), forCellReuseIdentifier: unknownCell)
}
// MARK: - DiffableTimelineLikeTableViewController
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)
}
}
}
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,
@ -100,112 +179,6 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
}
}
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? {
@ -238,12 +211,6 @@ class NotificationsTableViewController: DiffableTimelineLikeTableViewController<
}
}
extension NotificationsTableViewController {
enum Section: CaseIterable, Hashable {
case notifications
}
}
extension NotificationsTableViewController: TuskerNavigationDelegate {
var apiController: MastodonController { mastodonController }
}
@ -257,8 +224,7 @@ extension NotificationsTableViewController: StatusTableViewCellDelegate {
extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
for notification in group.notifications {
for notification in item(for: indexPath).notifications {
ImageCache.avatars.fetchIfNotCached(notification.account.avatar)
}
}
@ -266,8 +232,7 @@ extension NotificationsTableViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
for indexPath in indexPaths {
guard let group = dataSource.itemIdentifier(for: indexPath) else { continue }
for notification in group.notifications {
for notification in item(for: indexPath).notifications {
ImageCache.avatars.cancelWithoutCallback(notification.account.avatar)
}
}

View File

@ -29,8 +29,6 @@ class InstanceSelectorTableViewController: UITableViewController {
var urlHandler: AnyCancellable?
var currentQuery: String?
private var activityIndicator: UIActivityIndicatorView!
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .portrait
@ -52,15 +50,10 @@ 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 {
@ -80,19 +73,12 @@ class InstanceSelectorTableViewController: UITableViewController {
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.searchTextField.autocapitalizationType = .none
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
urlHandler = urlCheckerSubject
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.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) }
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.sink(receiveValue: updateSpecificInstance)
loadRecommendedInstances()
}
@ -126,8 +112,6 @@ class InstanceSelectorTableViewController: UITableViewController {
}
private func updateSpecificInstance(domain: String) {
activityIndicator.startAnimating()
let components = parseURLComponents(input: domain)
let url = components.url!
@ -136,26 +120,16 @@ class InstanceSelectorTableViewController: UITableViewController {
client.run(request) { (response) in
var snapshot = self.dataSource.snapshot()
if snapshot.indexOfSection(.selected) != nil {
snapshot.deleteSections([.selected])
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .selected))
}
if case let .success(instance, _) = response {
if snapshot.indexOfSection(.recommendedInstances) != nil {
snapshot.insertSections([.selected], beforeSection: .recommendedInstances)
} else {
if !snapshot.sectionIdentifiers.contains(.selected) {
snapshot.appendSections([.selected])
}
snapshot.appendItems([.selected(url, instance)], toSection: .selected)
DispatchQueue.main.async {
self.dataSource.apply(snapshot) {
self.activityIndicator.stopAnimating()
}
}
} else {
DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
self.dataSource.apply(snapshot)
}
}
}
@ -163,69 +137,14 @@ class InstanceSelectorTableViewController: UITableViewController {
private func loadRecommendedInstances() {
InstanceSelector.getInstances(category: nil) { (response) in
DispatchQueue.main.async {
switch response {
case let .failure(error):
self.showRecommendationsError(error)
case let .success(instances, _):
guard case let .success(instances, _) = response else { fatalError() }
self.recommendedInstances = instances
self.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() {
func filterRecommendedResults() {
let filteredInstances: [InstanceSelector.Instance]
if let currentQuery = currentQuery, !currentQuery.isEmpty {
filteredInstances = recommendedInstances.filter {
@ -236,21 +155,13 @@ class InstanceSelectorTableViewController: UITableViewController {
}
var snapshot = self.dataSource.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.deleteSections([.recommendedInstances])
snapshot.appendSections([.recommendedInstances])
}
snapshot.appendItems(filteredInstances.map { Item.recommended($0) }, toSection: .recommendedInstances)
DispatchQueue.main.async {
self.dataSource.apply(snapshot)
}
}
// MARK: - Table view delegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -283,30 +194,29 @@ extension InstanceSelectorTableViewController {
case recommended(InstanceSelector.Instance)
static func ==(lhs: Item, rhs: Item) -> Bool {
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
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
}
return false
}
func hash(into hasher: inout Hasher) {
switch self {
case let .selected(url, instance):
hasher.combine(0)
hasher.combine(Section.selected)
hasher.combine(url)
hasher.combine(instance.uri)
case let .recommended(instance):
hasher.combine(1)
hasher.combine(Section.recommendedInstances)
hasher.combine(instance.domain)
}
}
}
class DataSource: UITableViewDiffableDataSource<Section, Item> {
}
}

View File

@ -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: { [weak self] (_) in
self?.composeDirectMentioning()
UIAction(title: "Direct Message", image: UIImage(systemName: Status.Visibility.direct.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.composeDirectMentioning()
})
])
composeButton.isEnabled = mastodonController.loggedIn

View File

@ -20,7 +20,6 @@ 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
@ -59,42 +58,6 @@ 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
@ -124,17 +87,6 @@ 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
}
}
@ -156,9 +108,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
self.older = pagination?.older
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
DispatchQueue.main.async {
var snapshot = self.dataSource.snapshot()
snapshot.deleteSections([.statuses, .footer])
var snapshot = Snapshot()
snapshot.appendSections([.statuses, .footer])
snapshot.appendItems(statuses.map { .status(id: $0.id, state: .unknown) }, toSection: .statuses)
completion(.success(snapshot))
@ -166,9 +116,8 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
}
}
}
}
override func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
override func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let older = older else {
completion(.failure(.noOlder))
return
@ -176,12 +125,12 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
if #available(iOS 15.0, *),
Preferences.shared.disableInfiniteScrolling && !didConfirmLoadMore {
var snapshot = currentSnapshot()
guard !snapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
guard !currentSnapshot.itemIdentifiers(inSection: .footer).contains(.confirmLoadMore) else {
// todo: need something more accurate than "success"/"failure"
completion(.success(snapshot))
completion(.success(currentSnapshot))
return
}
var snapshot = currentSnapshot
snapshot.appendItems([.confirmLoadMore], toSection: .footer)
self.dataSource.apply(snapshot)
completion(.success(snapshot))
@ -199,7 +148,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))
@ -208,7 +157,7 @@ class TimelineTableViewController: DiffableTimelineLikeTableViewController<Timel
}
}
override func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
override func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
guard let newer = newer else {
completion(.failure(.noNewer))
return
@ -221,11 +170,6 @@ 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 {
@ -233,7 +177,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)
@ -252,28 +196,16 @@ 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) {
@ -281,8 +213,6 @@ extension TimelineTableViewController {
return a == b
case (.confirmLoadMore, .confirmLoadMore):
return true
case let (.publicTimelineDescription(local: a), .publicTimelineDescription(local: b)):
return a == b
default:
return false
}
@ -295,9 +225,6 @@ extension TimelineTableViewController {
hasher.combine(id)
case .confirmLoadMore:
hasher.combine(1)
case let .publicTimelineDescription(local: local):
hasher.combine(2)
hasher.combine(local)
}
}
}

View File

@ -59,7 +59,6 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
super.viewDidDisappear(animated)
pruneOffscreenRows()
currentToast?.dismissToast(animated: false)
}
class func refreshCommandTitle() -> String {
@ -112,27 +111,13 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
state = .loadingInitial
loadInitialItems() { result in
guard case let .success(snapshot) = result else {
self.state = .unloaded
return
}
DispatchQueue.main.async {
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
}
}
}
}
@ -147,28 +132,14 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
state = .loadingOlder
loadOlderItems(currentSnapshot: dataSource.snapshot) { result in
DispatchQueue.main.async {
loadOlderItems(currentSnapshot: dataSource.snapshot()) { result in
guard case let .success(snapshot) = result else {
self.state = .loaded
switch result {
case let .success(snapshot):
return
}
DispatchQueue.main.async {
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
}
self.state = .loaded
}
}
}
@ -185,9 +156,8 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
// this assumes that indexPathsForVisibleRows is always in order
lastLastVisibleRow = tableView.indexPathsForVisibleRows?.last
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.enumerated().filter { timelineContentSections().contains($0.element) }
if let lastContentSection = orderedContentSections.last,
indexPath.section == lastContentSection.offset,
let orderedContentSections = dataSource.snapshot().sectionIdentifiers.filter { timelineContentSections().contains($0) }
if indexPath.section == orderedContentSections.count - 1,
indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 {
loadOlder()
@ -213,58 +183,35 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
state = .loadingNewer
var firstItem: Item? = nil
let currentSnapshot: () -> Snapshot = {
let snapshot = self.dataSource.snapshot()
let snapshot = dataSource.snapshot()
for section in self.timelineContentSections() {
if snapshot.indexOfSection(section) != nil,
let first = snapshot.itemIdentifiers(inSection: section).first {
firstItem = first
var item: Item? = nil
for section in timelineContentSections() {
if let first = snapshot.itemIdentifiers(inSection: section).first {
item = first
break
}
}
return snapshot
}
loadNewerItems(currentSnapshot: currentSnapshot) { result in
loadNewerItems(currentSnapshot: snapshot) { result in
guard case let .success(snapshot) = result else {
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
self.state = .loaded
}
return
}
switch result {
case let .success(snapshot):
DispatchQueue.main.async {
self.refreshControl?.endRefreshing()
self.dataSource.apply(snapshot, animatingDifferences: false)
if let firstItem = firstItem,
let indexPath = self.dataSource.indexPath(for: firstItem) {
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)
}
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
}
}
}
}
@ -279,11 +226,11 @@ class DiffableTimelineLikeTableViewController<Section: Hashable & CaseIterable,
fatalError("loadInitialItems(completion:) must be implemented by subclasses")
}
func loadOlderItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
func loadOlderItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
fatalError("loadOlderItesm(completion:) must be implemented by subclasses")
}
func loadNewerItems(currentSnapshot: @escaping () -> Snapshot, completion: @escaping (LoadResult) -> Void) {
func loadNewerItems(currentSnapshot: Snapshot, completion: @escaping (LoadResult) -> Void) {
fatalError("loadNewerItems(completion:) must be implemented by subclasses")
}
@ -311,7 +258,6 @@ extension DiffableTimelineLikeTableViewController {
case noClient
case noOlder
case noNewer
case allCaughtUp
case client(Client.Error)
}
}
@ -319,20 +265,5 @@ 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"
}
}
}

View File

@ -67,9 +67,7 @@ extension MenuPreviewProvider {
guard let self = self,
case let .success(results, _) = response,
let relationship = results.first else {
DispatchQueue.main.async {
elementHandler([])
}
return
}
let following = relationship.following

View File

@ -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: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.pointSize, weight: .regular), range: attributed.fullRange)
case "pre":
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: self.font!.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: defaultFont.pointSize, weight: .regular)])
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: self.font!.pointSize, weight: .regular)])
} else if parentTag == "ul" {
bullet = NSAttributedString(string: "\u{2022}\t")
} else {

View File

@ -1,57 +0,0 @@
//
// 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?()
}
}

View File

@ -1,48 +0,0 @@
<?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>

View File

@ -1,33 +0,0 @@
//
// 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
}
}

View File

@ -1,255 +0,0 @@
//
// 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
}
}
}

View File

@ -1,105 +0,0 @@
//
// 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, &currentToastKey) as? WeakHolder<ToastView>
return holder?.object
}
set {
if let newValue = newValue {
objc_setAssociatedObject(self, &currentToastKey, WeakHolder(object: newValue), .OBJC_ASSOCIATION_RETAIN)
} else {
objc_setAssociatedObject(self, &currentToastKey, 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
}
}