Compare commits
No commits in common. "addcc2daccb20f3bd9c9a0f0492840520587e367" and "b1421767dd98331373cf0244a40b7e62168f4789" have entirely different histories.
addcc2dacc
...
b1421767dd
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,17 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## 2023.2 (66)
|
||||
Features/Improvements:
|
||||
- Improve design of link preview card
|
||||
- Show loading indicator during timeline state restoration
|
||||
|
||||
Bugfixes:
|
||||
- iPadOS/macOS: Fix some keyboard shortcuts not working
|
||||
- Fix crash when restoring timeline state
|
||||
- Fix status collapse button disappearing when navigating away
|
||||
- Fix crash when status swipe action takes too long to complete
|
||||
- Fix tapping expand thread cell not working
|
||||
|
||||
## 2023.1 (64)
|
||||
Features/Improvements:
|
||||
- Add Delete Post action to statuses
|
||||
|
|
|
@ -18,9 +18,6 @@
|
|||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||
D601FA5B29787AB100A8E8B5 /* AccountFollowsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */; };
|
||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */; };
|
||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */; };
|
||||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */; };
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
|
||||
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
|
||||
|
@ -166,6 +163,8 @@
|
|||
D662AEEF263A3B880082A153 /* PollFinishedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */; };
|
||||
D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */; };
|
||||
D662AEF2263A4BE10082A153 /* ComposePollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D662AEF1263A4BE10082A153 /* ComposePollView.swift */; };
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */; };
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */; };
|
||||
D663626221360B1900C9CBA2 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626121360B1900C9CBA2 /* Preferences.swift */; };
|
||||
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626321360D2300C9CBA2 /* AvatarStyle.swift */; };
|
||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||
|
@ -174,6 +173,7 @@
|
|||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
||||
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
|
||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */; };
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||
|
@ -293,6 +293,8 @@
|
|||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */; };
|
||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */; };
|
||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */; };
|
||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||
D6C99FCB24FADC91005C74D3 /* MainComposeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */; };
|
||||
|
@ -414,9 +416,6 @@
|
|||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||
D601FA5A29787AB100A8E8B5 /* AccountFollowsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFollowsListViewController.swift; sourceTree = "<group>"; };
|
||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagSearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||
D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendHistoryView.swift; sourceTree = "<group>"; };
|
||||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -562,6 +561,8 @@
|
|||
D662AEED263A3B880082A153 /* PollFinishedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D662AEEE263A3B880082A153 /* PollFinishedTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PollFinishedTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D662AEF1263A4BE10082A153 /* ComposePollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposePollView.swift; sourceTree = "<group>"; };
|
||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConversationMainStatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMainStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D663626121360B1900C9CBA2 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D663626321360D2300C9CBA2 /* AvatarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStyle.swift; sourceTree = "<group>"; };
|
||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
||||
|
@ -569,6 +570,7 @@
|
|||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||
D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
|
||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTableViewController.swift; sourceTree = "<group>"; };
|
||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -689,6 +691,8 @@
|
|||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandThreadTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ExpandThreadTableViewCell.xib; sourceTree = "<group>"; };
|
||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||
D6C99FCA24FADC91005C74D3 /* MainComposeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainComposeTextView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1045,8 +1049,9 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D65B4B6329771EFF00DABDFB /* ConversationViewController.swift */,
|
||||
D601FA5C297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift */,
|
||||
D601FA5E297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift */,
|
||||
D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */,
|
||||
D6C82B5425C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift */,
|
||||
D6C82B5525C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib */,
|
||||
);
|
||||
path = Conversation;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1125,12 +1130,13 @@
|
|||
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */,
|
||||
D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */,
|
||||
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */,
|
||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
|
||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
|
||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
|
||||
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */,
|
||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */,
|
||||
D61ABEF528EE74D400B29151 /* StatusCollectionViewCell.swift */,
|
||||
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */,
|
||||
D601FA60297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift */,
|
||||
D6ADB6EB28EA73CB009924AB /* StatusContentContainer.swift */,
|
||||
);
|
||||
path = Status;
|
||||
|
@ -1798,10 +1804,12 @@
|
|||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
||||
D6C82B5725C5F3F20017F1E6 /* ExpandThreadTableViewCell.xib in Resources */,
|
||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||
D663625D2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib in Resources */,
|
||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
||||
D626493D23C1000300612E6E /* AlbumTableViewCell.xib in Resources */,
|
||||
|
@ -1926,6 +1934,7 @@
|
|||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||
D62275AA24F1E01C00B82A16 /* ComposeTextView.swift in Sources */,
|
||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */,
|
||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
|
||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */,
|
||||
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
|
||||
|
@ -2077,6 +2086,7 @@
|
|||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */,
|
||||
D653F411267D1E32004E32B1 /* DiffableTimelineLikeTableViewController.swift in Sources */,
|
||||
D6C82B5625C5F3F20017F1E6 /* ExpandThreadTableViewCell.swift in Sources */,
|
||||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||
D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */,
|
||||
|
@ -2095,9 +2105,7 @@
|
|||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,
|
||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
|
||||
D601FA61297B539E00A8E8B5 /* ConversationMainStatusCollectionViewCell.swift in Sources */,
|
||||
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
|
||||
D601FA5F297B339100A8E8B5 /* ExpandThreadCollectionViewCell.swift in Sources */,
|
||||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||
|
@ -2171,12 +2179,12 @@
|
|||
D677284E24ECC01D00C732D3 /* Draft.swift in Sources */,
|
||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||
D601FA5D297B2E6F00A8E8B5 /* ConversationCollectionViewController.swift in Sources */,
|
||||
D6D12B56292D57E800D528E1 /* AccountCollectionViewCell.swift in Sources */,
|
||||
D6A6C10525B6138A00298D0F /* StatusTablePrefetching.swift in Sources */,
|
||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||
|
@ -2342,7 +2350,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2350,7 +2358,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2410,7 +2418,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2419,7 +2427,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -2561,7 +2569,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2569,7 +2577,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
OTHER_LDFLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
|
@ -2590,7 +2598,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = Tusker/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
|
@ -2598,7 +2606,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -2700,7 +2708,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2709,7 +2717,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -2726,7 +2734,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 66;
|
||||
CURRENT_PROJECT_VERSION = 65;
|
||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.3;
|
||||
|
@ -2735,7 +2743,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2023.2;
|
||||
MARKETING_VERSION = 2023.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).Tusker.OpenInTusker";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
|
|
@ -1,406 +0,0 @@
|
|||
//
|
||||
// ConversationCollectionViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/28/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class ConversationNode {
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationCollectionViewController: UIViewController, CollectionViewController {
|
||||
|
||||
private let mastodonController: MastodonController
|
||||
private let mainStatusID: String
|
||||
private let mainStatusState: CollapseState
|
||||
var statusIDToScrollToOnLoad: String
|
||||
var showStatusesAutomatically = false
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
view as? UICollectionView
|
||||
}
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(for mainStatusID: String, state: CollapseState, mastodonController: MastodonController) {
|
||||
self.mainStatusID = mainStatusID
|
||||
self.mainStatusState = state
|
||||
self.statusIDToScrollToOnLoad = mainStatusID
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
var config = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
config.backgroundColor = .secondarySystemBackground
|
||||
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
|
||||
}
|
||||
config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
|
||||
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
|
||||
}
|
||||
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
|
||||
let rowsInSection = self.collectionView.numberOfItems(inSection: indexPath.section)
|
||||
let lastInSection = indexPath.row == rowsInSection - 1
|
||||
var config = sectionConfig
|
||||
config.topSeparatorVisibility = .hidden
|
||||
config.bottomSeparatorVisibility = lastInSection ? .visible : .hidden
|
||||
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
|
||||
return config
|
||||
}
|
||||
// we're not using contenetInsetsReference = .readableContent here because it always insets the cells even if
|
||||
// the collection view's actual width is narrow enough to fit in the readable width, resulting in a bit of the
|
||||
// background color always peaking through the edges
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: config)
|
||||
view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||
// something about the autoresizing mask breaks resizing the vc
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
collectionView.delegate = self
|
||||
collectionView.dragDelegate = self
|
||||
collectionView.allowsFocus = true
|
||||
|
||||
dataSource = createDataSource()
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Bool, Bool)> { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||
cell.showReplyIndicator = false
|
||||
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
|
||||
cell.setShowThreadLinks(prev: item.2, next: item.3)
|
||||
}
|
||||
let mainStatusCell = UICollectionView.CellRegistration<ConversationMainStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in
|
||||
cell.delegate = self
|
||||
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||
cell.updateUI(statusID: item.0, state: item.1)
|
||||
cell.setShowThreadLinks(prev: item.2, next: false)
|
||||
}
|
||||
let expandThreadCell = UICollectionView.CellRegistration<ExpandThreadCollectionViewCell, ([ConversationNode], Bool)> { cell, indexPath, item in
|
||||
cell.updateUI(childThreads: item.0, inline: item.1)
|
||||
}
|
||||
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
|
||||
switch itemIdentifier {
|
||||
case let .status(id: id, state: state, prevLink: prevLink, nextLink: nextLink):
|
||||
if id == self.mainStatusID {
|
||||
return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
|
||||
} else {
|
||||
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, prevLink, nextLink))
|
||||
}
|
||||
case .expandThread(childThreads: let childThreads, inline: let inline):
|
||||
return collectionView.dequeueConfiguredReusableCell(using: expandThreadCell, for: indexPath, item: (childThreads, inline))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addMainStatus(_ status: StatusMO) {
|
||||
loadViewIfNeeded()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
|
||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||
let parentIDs = getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||
|
||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||
|
||||
var snapshot = dataSource.snapshot()
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
|
||||
let parentItems = parentIDs.enumerated().map { index, id in
|
||||
Item.status(id: id, state: .unknown, prevLink: index > 0, nextLink: true)
|
||||
}
|
||||
snapshot.insertItems(parentItems, beforeItem: mainStatusItem)
|
||||
snapshot.reloadItems([mainStatusItem])
|
||||
|
||||
// fetch all descendant status managed objects
|
||||
let descendantIDs = context.descendants.map(\.id)
|
||||
let request = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id IN %@", descendantIDs)
|
||||
|
||||
if let descendants = try? mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||
// convert array of descendant statuses into tree of sub-threads
|
||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
}
|
||||
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
let item: Item
|
||||
let position: UICollectionView.ScrollPosition
|
||||
if self.statusIDToScrollToOnLoad == self.mainStatusID {
|
||||
item = mainStatusItem
|
||||
position = .centeredVertically
|
||||
} else {
|
||||
item = snapshot.itemIdentifiers.first {
|
||||
if case .status(id: self.statusIDToScrollToOnLoad, _, _, _) = $0 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}!
|
||||
position = .top
|
||||
}
|
||||
// ensure that the status is on-screen after newly loaded statuses are added
|
||||
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||
if let indexPath = self.dataSource.indexPath(for: item) {
|
||||
self.collectionView.scrollToItem(at: indexPath, at: position, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
var statuses = statuses
|
||||
var parents = [String]()
|
||||
|
||||
var parentID: String? = inReplyToID
|
||||
|
||||
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
||||
let parentStatus = statuses.remove(at: parentIndex)
|
||||
parents.insert(parentStatus.id, at: 0)
|
||||
parentID = parentStatus.inReplyToID
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||
var descendants = descendants
|
||||
|
||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||
descendants.removeAll { $0.inReplyToID == id }
|
||||
return statuses
|
||||
}
|
||||
|
||||
var nodes: [String: ConversationNode] = [
|
||||
mainStatus.id: ConversationNode(status: mainStatus)
|
||||
]
|
||||
|
||||
var idsToCheck = [mainStatusID]
|
||||
|
||||
while !idsToCheck.isEmpty {
|
||||
let inReplyToID = idsToCheck.removeFirst()
|
||||
let nodeForID = nodes[inReplyToID]!
|
||||
|
||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||
for reply in inReply {
|
||||
idsToCheck.append(reply.id)
|
||||
|
||||
let replyNode = ConversationNode(status: reply)
|
||||
nodes[reply.id] = replyNode
|
||||
|
||||
nodeForID.children.append(replyNode)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes[mainStatusID]!.children
|
||||
}
|
||||
|
||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||
var childThreads = childThreads
|
||||
|
||||
// child threads by the same author as the main status come first
|
||||
let pivotIndex = childThreads.partition(by: { $0.status.account.id != mainStatus.account.id })
|
||||
|
||||
// within each group, child threads are sorted chronologically
|
||||
childThreads[0..<pivotIndex].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
childThreads[pivotIndex...].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
|
||||
for node in childThreads {
|
||||
let section = Section.childThread(firstStatusID: node.status.id)
|
||||
snapshot.appendSections([section])
|
||||
snapshot.appendItems([.status(id: node.status.id, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
|
||||
|
||||
var currentNode = node
|
||||
while true {
|
||||
let next: ConversationNode
|
||||
|
||||
if currentNode.children.count == 0 {
|
||||
break
|
||||
} else if currentNode.children.count == 1 {
|
||||
next = currentNode.children[0]
|
||||
} else {
|
||||
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||
if sameAuthorStatuses.count == 1 {
|
||||
next = sameAuthorStatuses[0]
|
||||
let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
|
||||
snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
|
||||
} else {
|
||||
snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = next
|
||||
snapshot.appendItems([.status(id: next.status.id, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateVisibleCellCollapseState() {
|
||||
var snapshot = dataSource.snapshot()
|
||||
var cellsToMask: [StatusCollectionViewCell] = []
|
||||
for item in snapshot.itemIdentifiers {
|
||||
guard case .status(id: _, state: let state, prevLink: _, nextLink: _) = item,
|
||||
state.collapsible == true else {
|
||||
continue
|
||||
}
|
||||
state.collapsed = !showStatusesAutomatically
|
||||
snapshot.reconfigureItems([item])
|
||||
|
||||
if let indexPath = dataSource.indexPath(for: item),
|
||||
let cell = collectionView.cellForItem(at: indexPath) as? StatusCollectionViewCell {
|
||||
cellsToMask.append(cell)
|
||||
}
|
||||
}
|
||||
for cell in cellsToMask {
|
||||
cell.contentContainer.layer.masksToBounds = true
|
||||
}
|
||||
dataSource.apply(snapshot, animatingDifferences: true) {
|
||||
// this is an absurdly long delay, I have no idea why it's necessary
|
||||
// without it, the layer is not maksed during the animation
|
||||
// unless there's only one cell
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
||||
for cell in cellsToMask {
|
||||
cell.contentContainer.layer.masksToBounds = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController {
|
||||
enum Section: Hashable {
|
||||
case statuses
|
||||
case childThread(firstStatusID: String)
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: CollapseState, prevLink: Bool, nextLink: Bool)
|
||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||
|
||||
static func ==(lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(id: a, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, state: _, prevLink: bPrev, nextLink: bNext)):
|
||||
return a == b && aPrev == bPrev && aNext == bNext
|
||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||
return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .status(id: id, state: _, prevLink: prevLink, nextLink: nextLink):
|
||||
hasher.combine(0)
|
||||
hasher.combine(id)
|
||||
hasher.combine(prevLink)
|
||||
hasher.combine(nextLink)
|
||||
case .expandThread(childThreads: let childThreads, inline: let inline):
|
||||
hasher.combine(1)
|
||||
for thread in childThreads {
|
||||
hasher.combine(thread.status.id)
|
||||
}
|
||||
hasher.combine(inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||
if case .status(id: let id, _, _, _) = dataSource.itemIdentifier(for: indexPath),
|
||||
id == mainStatusID {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
switch dataSource.itemIdentifier(for: indexPath) {
|
||||
case nil:
|
||||
break
|
||||
case .status(id: let id, state: let state, _, _):
|
||||
selected(status: id, state: state.copy())
|
||||
case .expandThread(childThreads: let childThreads, inline: _):
|
||||
if case .status(id: let id, state: let state, _, _) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||
// todo: it would be nice to avoid re-fetching the context here, since we should have all the necessary information already
|
||||
let conv = ConversationViewController(for: id, state: state.copy(), mastodonController: mastodonController)
|
||||
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||
show(conv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: UICollectionViewDragDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: MenuActionProvider {
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: StatusCollectionViewCellDelegate {
|
||||
func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
|
||||
if let indexPath = collectionView.indexPath(for: cell) {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
||||
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
|
||||
// todo: support filtering in conversations
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: TabBarScrollableViewController {
|
||||
func tabBarScrollToTop() {
|
||||
collectionView.scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationCollectionViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
collectionView.scrollToTop()
|
||||
return .stop
|
||||
}
|
||||
}
|
|
@ -0,0 +1,381 @@
|
|||
//
|
||||
// ConversationTableViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/28/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import CoreData
|
||||
|
||||
class ConversationNode {
|
||||
let status: StatusMO
|
||||
var children: [ConversationNode]
|
||||
|
||||
init(status: StatusMO) {
|
||||
self.status = status
|
||||
self.children = []
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationTableViewController: EnhancedTableViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
let mainStatusID: String
|
||||
let mainStatusState: CollapseState
|
||||
var statusIDToScrollToOnLoad: String
|
||||
private(set) var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
|
||||
var showStatusesAutomatically = false
|
||||
|
||||
init(for mainStatusID: String, state: CollapseState, mastodonController: MastodonController) {
|
||||
self.mainStatusID = mainStatusID
|
||||
self.mainStatusState = state
|
||||
self.statusIDToScrollToOnLoad = mainStatusID
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
super.init(style: .plain)
|
||||
|
||||
dragEnabled = true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.prefetchDataSource = self
|
||||
|
||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
|
||||
tableView.register(UINib(nibName: "ExpandThreadTableViewCell", bundle: .main), forCellReuseIdentifier: "expandThreadCell")
|
||||
|
||||
tableView.allowsFocus = true
|
||||
tableView.backgroundColor = .secondarySystemBackground
|
||||
// separators are disabled on the table view so we can re-add them ourselves
|
||||
// so they're not inserted in between statuses in the ame sub-thread
|
||||
tableView.separatorStyle = .none
|
||||
tableView.cellLayoutMarginsFollowReadableWidth = UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac
|
||||
|
||||
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
|
||||
switch item {
|
||||
case let .status(id: id, state: state):
|
||||
let rowsInSection = self.dataSource.tableView(tableView, numberOfRowsInSection: indexPath.section)
|
||||
let firstInSection = indexPath.row == 0
|
||||
let lastInSection = indexPath.row == rowsInSection - 1
|
||||
|
||||
let identifier = id == self.mainStatusID ? "mainStatusCell" : "statusCell"
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! BaseStatusTableViewCell
|
||||
|
||||
if id == self.mainStatusID {
|
||||
cell.selectionStyle = .none
|
||||
}
|
||||
|
||||
cell.delegate = self
|
||||
cell.showStatusAutomatically = self.showStatusesAutomatically
|
||||
|
||||
if let cell = cell as? TimelineStatusTableViewCell {
|
||||
cell.showReplyIndicator = false
|
||||
}
|
||||
|
||||
cell.updateUI(statusID: id, state: state)
|
||||
|
||||
cell.setShowThreadLinks(prev: !firstInSection, next: !lastInSection)
|
||||
if lastInSection {
|
||||
if cell.viewWithTag(ViewTags.conversationBottomSeparator) == nil {
|
||||
let separator = UIView()
|
||||
separator.tag = ViewTags.conversationBottomSeparator
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
separator.backgroundColor = tableView.separatorColor
|
||||
cell.addSubview(separator)
|
||||
NSLayoutConstraint.activate([
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
separator.bottomAnchor.constraint(equalTo: cell.bottomAnchor),
|
||||
separator.leftAnchor.constraint(equalTo: cell.leftAnchor, constant: cell.separatorInset.left),
|
||||
separator.rightAnchor.constraint(equalTo: cell.rightAnchor),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
cell.viewWithTag(ViewTags.conversationBottomSeparator)?.removeFromSuperview()
|
||||
}
|
||||
|
||||
return cell
|
||||
|
||||
case let .expandThread(childThreads: childThreads, inline: inline):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "expandThreadCell", for: indexPath) as! ExpandThreadTableViewCell
|
||||
cell.updateUI(childThreads: childThreads, inline: inline)
|
||||
return cell
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func addMainStatus(_ status: StatusMO) {
|
||||
loadViewIfNeeded()
|
||||
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
func addContext(_ context: ConversationContext, for mainStatus: StatusMO) async {
|
||||
let parentIDs = self.getDirectParents(inReplyTo: mainStatus.inReplyToID, from: context.ancestors)
|
||||
let parentStatuses = context.ancestors.filter { parentIDs.contains($0.id) }
|
||||
|
||||
await mastodonController.persistentContainer.addAll(statuses: parentStatuses + context.descendants)
|
||||
|
||||
let mainStatusItem = Item.status(id: mainStatusID, state: mainStatusState)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.statuses])
|
||||
snapshot.appendItems([mainStatusItem], toSection: .statuses)
|
||||
snapshot.insertItems(parentIDs.map { .status(id: $0, state: .unknown) }, beforeItem: mainStatusItem)
|
||||
|
||||
// fetch all descendant status managed objects
|
||||
let descendantIDs = context.descendants.map(\.id)
|
||||
let request: NSFetchRequest<StatusMO> = StatusMO.fetchRequest()
|
||||
request.predicate = NSPredicate(format: "id in %@", descendantIDs)
|
||||
|
||||
if let descendants = try? self.mastodonController.persistentContainer.viewContext.fetch(request) {
|
||||
// convert array of descendant statuses into tree of sub-threads
|
||||
let childThreads = self.getDescendantThreads(mainStatus: mainStatus, descendants: descendants)
|
||||
|
||||
// convert sub-threads into items for section and add to snapshot
|
||||
self.addFlattenedChildThreadsToSnapshot(childThreads, mainStatus: mainStatus, snapshot: &snapshot)
|
||||
}
|
||||
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false) {
|
||||
let item: Item
|
||||
let position: UITableView.ScrollPosition
|
||||
if self.statusIDToScrollToOnLoad == self.mainStatusID {
|
||||
item = mainStatusItem
|
||||
position = .middle
|
||||
} else {
|
||||
item = Item.status(id: self.statusIDToScrollToOnLoad, state: .unknown)
|
||||
position = .top
|
||||
}
|
||||
// ensure that the status is on-screen after newly loaded statuses are added
|
||||
// todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
|
||||
if let indexPath = self.dataSource.indexPath(for: item) {
|
||||
self.tableView.scrollToRow(at: indexPath, at: position, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func getDirectParents(inReplyTo inReplyToID: String?, from statuses: [Status]) -> [String] {
|
||||
var statuses = statuses
|
||||
var parents = [String]()
|
||||
|
||||
var parentID: String? = inReplyToID
|
||||
|
||||
while parentID != nil, let parentIndex = statuses.firstIndex(where: { $0.id == parentID }) {
|
||||
let parentStatus = statuses.remove(at: parentIndex)
|
||||
parents.insert(parentStatus.id, at: 0)
|
||||
parentID = parentStatus.inReplyToID
|
||||
}
|
||||
|
||||
return parents
|
||||
}
|
||||
|
||||
private func getDescendantThreads(mainStatus: StatusMO, descendants: [StatusMO]) -> [ConversationNode] {
|
||||
var descendants = descendants
|
||||
|
||||
func removeAllInReplyTo(id: String) -> [StatusMO] {
|
||||
let statuses = descendants.filter { $0.inReplyToID == id }
|
||||
descendants.removeAll { $0.inReplyToID == id }
|
||||
return statuses
|
||||
}
|
||||
|
||||
var nodes: [String: ConversationNode] = [
|
||||
mainStatus.id: ConversationNode(status: mainStatus)
|
||||
]
|
||||
|
||||
var idsToCheck = [mainStatusID]
|
||||
|
||||
while !idsToCheck.isEmpty {
|
||||
let inReplyToID = idsToCheck.removeFirst()
|
||||
let nodeForID = nodes[inReplyToID]!
|
||||
|
||||
let inReply = removeAllInReplyTo(id: inReplyToID)
|
||||
for reply in inReply {
|
||||
idsToCheck.append(reply.id)
|
||||
|
||||
let replyNode = ConversationNode(status: reply)
|
||||
nodes[reply.id] = replyNode
|
||||
|
||||
nodeForID.children.append(replyNode)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes[mainStatusID]!.children
|
||||
}
|
||||
|
||||
private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
|
||||
var childThreads = childThreads
|
||||
|
||||
// child threads by the same author as the main status come first
|
||||
let pivotIndex = childThreads.partition(by: { $0.status.account.id != mainStatus.account.id })
|
||||
|
||||
// within each group, child threads are sorted chronologically
|
||||
childThreads[0..<pivotIndex].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
childThreads[pivotIndex...].sort(by: { $0.status.createdAt < $1.status.createdAt })
|
||||
|
||||
for node in childThreads {
|
||||
snapshot.appendSections([.childThread(firstStatusID: node.status.id)])
|
||||
snapshot.appendItems([.status(id: node.status.id, state: .unknown)])
|
||||
|
||||
var currentNode = node
|
||||
while true {
|
||||
let next: ConversationNode
|
||||
|
||||
if currentNode.children.count == 0 {
|
||||
break
|
||||
} else if currentNode.children.count == 1 {
|
||||
next = currentNode.children[0]
|
||||
} else {
|
||||
let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
|
||||
if sameAuthorStatuses.count == 1 {
|
||||
next = sameAuthorStatuses[0]
|
||||
let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
|
||||
snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
|
||||
} else {
|
||||
snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = next
|
||||
snapshot.appendItems([.status(id: next.status.id, state: .unknown)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(for indexPath: IndexPath) -> (id: String, state: CollapseState)? {
|
||||
return self.dataSource.itemIdentifier(for: indexPath).flatMap { (item) in
|
||||
switch item {
|
||||
case let .status(id: id, state: state):
|
||||
return (id: id, state: state)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if case let .expandThread(childThreads: childThreads, inline: _) = dataSource.itemIdentifier(for: indexPath),
|
||||
case let .status(id: id, state: state) = dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)) {
|
||||
let conv = ConversationViewController(for: id, state: state, mastodonController: mastodonController)
|
||||
conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
|
||||
conv.showStatusesAutomatically = showStatusesAutomatically
|
||||
show(conv)
|
||||
} else {
|
||||
super.tableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.leadingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
return (tableView.cellForRow(at: indexPath) as? TableViewSwipeActionProvider)?.trailingSwipeActionsConfiguration()
|
||||
}
|
||||
|
||||
func updateVisibleCellCollapseState() {
|
||||
let snapshot = dataSource.snapshot()
|
||||
for case let .status(id: _, state: state) in snapshot.itemIdentifiers where state.collapsible == true {
|
||||
state.collapsed = !showStatusesAutomatically
|
||||
}
|
||||
|
||||
for cell in tableView.visibleCells {
|
||||
guard let cell = cell as? BaseStatusTableViewCell,
|
||||
cell.collapsible else { continue }
|
||||
cell.showStatusAutomatically = showStatusesAutomatically
|
||||
cell.setCollapsed(!showStatusesAutomatically, animated: false)
|
||||
}
|
||||
|
||||
// recalculate cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController {
|
||||
enum Section: Hashable {
|
||||
case statuses
|
||||
case childThread(firstStatusID: String)
|
||||
}
|
||||
enum Item: Hashable {
|
||||
case status(id: String, state: CollapseState)
|
||||
case expandThread(childThreads: [ConversationNode], inline: Bool)
|
||||
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.status(id: a, state: _), .status(id: b, state: _)):
|
||||
return a == b
|
||||
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
|
||||
return zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .status(id: id, state: _):
|
||||
hasher.combine("status")
|
||||
hasher.combine(id)
|
||||
case let .expandThread(childThreads: children, inline: inline):
|
||||
hasher.combine("expandThread")
|
||||
hasher.combine(children.map(\.status.id))
|
||||
hasher.combine(inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
|
||||
func conversation(mainStatusID: String, state: CollapseState) -> ConversationViewController {
|
||||
let vc = ConversationViewController(for: mainStatusID, state: state, mastodonController: mastodonController)
|
||||
// transfer show statuses automatically state when showing new conversation
|
||||
vc.showStatusesAutomatically = self.showStatusesAutomatically
|
||||
return vc
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: MenuActionProvider {
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: StatusTableViewCellDelegate {
|
||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||
// causes the table view to recalculate the cell heights
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: UITableViewDataSourcePrefetching, StatusTablePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
let ids: [String] = indexPaths.compactMap { item(for: $0)?.id }
|
||||
prefetchStatuses(with: ids)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationTableViewController: ToastableViewController {
|
||||
}
|
|
@ -171,7 +171,7 @@ class ConversationViewController: UIViewController {
|
|||
|
||||
@MainActor
|
||||
private func mainStatusLoaded(_ mainStatus: StatusMO) async {
|
||||
let vc = ConversationCollectionViewController(for: mainStatusID, state: mainStatusState, mastodonController: mastodonController)
|
||||
let vc = ConversationTableViewController(for: mainStatusID, state: mainStatusState, mastodonController: mastodonController)
|
||||
vc.statusIDToScrollToOnLoad = statusIDToScrollToOnLoad
|
||||
vc.showStatusesAutomatically = showStatusesAutomatically
|
||||
vc.addMainStatus(mainStatus)
|
||||
|
@ -244,7 +244,7 @@ extension ConversationViewController {
|
|||
enum State {
|
||||
case unloaded
|
||||
case loading(UIActivityIndicatorView)
|
||||
case displaying(ConversationCollectionViewController)
|
||||
case displaying(ConversationTableViewController)
|
||||
case notFound
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// ExpandThreadCollectionViewCell.swift
|
||||
// ExpandThreadTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/30/21.
|
||||
|
@ -8,94 +8,62 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class ExpandThreadCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
private var avatarContainerView: UIView!
|
||||
private var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||
|
||||
private var replyCountLabel: UILabel!
|
||||
|
||||
private var hStack: UIStackView!
|
||||
private var stackViewLeadingConstraint: NSLayoutConstraint!
|
||||
|
||||
class ExpandThreadTableViewCell: UITableViewCell {
|
||||
|
||||
@IBOutlet weak var stackViewLeadingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var avatarContainerView: UIView!
|
||||
@IBOutlet weak var avatarContainerWidthConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var replyCountLabel: UILabel!
|
||||
private var threadLinkView: UIView!
|
||||
private var threadLinkViewFullHeightConstraint: NSLayoutConstraint!
|
||||
private var threadLinkViewShortHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
private var avatarImageViews: [UIImageView] = []
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
avatarContainerView = UIView()
|
||||
avatarContainerView.backgroundColor = .clear
|
||||
avatarContainerWidthConstraint = avatarContainerView.widthAnchor.constraint(equalToConstant: 100)
|
||||
|
||||
replyCountLabel = UILabel()
|
||||
replyCountLabel.textColor = .tintColor
|
||||
replyCountLabel.font = .preferredFont(forTextStyle: .body)
|
||||
replyCountLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
hStack = UIStackView(arrangedSubviews: [
|
||||
avatarContainerView,
|
||||
replyCountLabel,
|
||||
])
|
||||
hStack.spacing = 8
|
||||
hStack.alignment = .center
|
||||
hStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(hStack)
|
||||
stackViewLeadingConstraint = hStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16)
|
||||
|
||||
// TODO: separator
|
||||
private var avatarRequests: [ImageCache.Request] = []
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
threadLinkView = UIView()
|
||||
threadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||
threadLinkView.backgroundColor = .tintColor.withAlphaComponent(0.5)
|
||||
threadLinkView.layer.cornerRadius = 2.5
|
||||
threadLinkView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(threadLinkView)
|
||||
threadLinkViewFullHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||
threadLinkViewShortHeightConstraint = threadLinkView.bottomAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: -2)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
avatarContainerWidthConstraint,
|
||||
avatarContainerView.heightAnchor.constraint(equalToConstant: 32),
|
||||
|
||||
stackViewLeadingConstraint,
|
||||
hStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
hStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||
hStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
|
||||
|
||||
threadLinkView.widthAnchor.constraint(equalToConstant: 5),
|
||||
threadLinkView.centerXAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: 25),
|
||||
threadLinkView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
threadLinkView.centerXAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16 + (50 / 2))
|
||||
threadLinkViewFullHeightConstraint,
|
||||
])
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateUI(childThreads: [ConversationNode], inline: Bool) {
|
||||
stackViewLeadingConstraint.constant = 16 + (inline ? 50 + 4 : 0)
|
||||
stackViewLeadingConstraint.constant = inline ? 50 + 4 : 0
|
||||
threadLinkView.layer.maskedCorners = inline ? [] : [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
threadLinkViewFullHeightConstraint.isActive = inline
|
||||
threadLinkViewShortHeightConstraint.isActive = !inline
|
||||
|
||||
let format: String
|
||||
if inline {
|
||||
format = NSLocalizedString("expand threads inline count", comment: "expand conversation threads inline button label")
|
||||
format = NSLocalizedString("expand threads inline count", comment: "expnad converstaion threads inline button label")
|
||||
} else {
|
||||
format = NSLocalizedString("expand threads count", comment: "expand conversation threads button label")
|
||||
}
|
||||
replyCountLabel.text = String.localizedStringWithFormat(format, childThreads.count)
|
||||
|
||||
let accounts = childThreads.map(\.status.account).uniques().prefix(3)
|
||||
|
||||
avatarImageViews.forEach { $0.removeFromSuperview() }
|
||||
avatarImageViews = []
|
||||
|
||||
avatarRequests = []
|
||||
|
||||
let avatarImageSize: CGFloat = 44 - 12
|
||||
|
||||
if accounts.count == 1 {
|
||||
avatarContainerWidthConstraint.constant = avatarImageSize
|
||||
} else {
|
||||
|
@ -103,7 +71,7 @@ class ExpandThreadCollectionViewCell: UICollectionViewCell {
|
|||
}
|
||||
|
||||
for (index, account) in accounts.enumerated() {
|
||||
let accountImageView = CachedImageView(cache: .avatars)
|
||||
let accountImageView = UIImageView()
|
||||
accountImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
accountImageView.contentMode = .scaleAspectFit
|
||||
accountImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
||||
|
@ -113,8 +81,9 @@ class ExpandThreadCollectionViewCell: UICollectionViewCell {
|
|||
// need a solid background color so semi-transparent avatars don't look bad
|
||||
accountImageView.backgroundColor = .secondarySystemBackground
|
||||
avatarContainerView.addSubview(accountImageView)
|
||||
avatarImageViews.append(accountImageView)
|
||||
|
||||
avatarImageViews.append(accountImageView)
|
||||
|
||||
accountImageView.layer.zPosition = CGFloat(-index)
|
||||
|
||||
let xConstraint: NSLayoutConstraint
|
||||
|
@ -129,17 +98,32 @@ class ExpandThreadCollectionViewCell: UICollectionViewCell {
|
|||
accountImageView.widthAnchor.constraint(equalToConstant: avatarImageSize),
|
||||
accountImageView.heightAnchor.constraint(equalToConstant: avatarImageSize),
|
||||
accountImageView.centerYAnchor.constraint(equalTo: avatarContainerView.centerYAnchor),
|
||||
xConstraint,
|
||||
xConstraint
|
||||
])
|
||||
|
||||
accountImageView.update(for: account.avatar)
|
||||
if let avatar = account.avatar {
|
||||
let req = ImageCache.avatars.get(avatar) { [weak accountImageView] (_, image) in
|
||||
DispatchQueue.main.async {
|
||||
accountImageView?.image = image
|
||||
}
|
||||
}
|
||||
if let req = req {
|
||||
avatarRequests.append(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
for view in avatarImageViews {
|
||||
view.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: view)
|
||||
avatarImageViews.forEach {
|
||||
$0.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: $0)
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
avatarRequests.forEach { $0.cancel() }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" 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="21505"/>
|
||||
<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" id="KGk-i7-Jjw" customClass="ExpandThreadTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" layoutMarginsFollowReadableWidth="YES" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="IXi-sc-YIw">
|
||||
<rect key="frame" x="16" y="0.0" width="173" height="44"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eFB-F1-d3A">
|
||||
<rect key="frame" x="0.0" y="6" width="100" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="g9U-u7-718"/>
|
||||
<constraint firstAttribute="width" constant="100" id="tiI-Rj-gjh"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="2 replies" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dcm-ll-GeE">
|
||||
<rect key="frame" x="108" y="12" width="65" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="tintColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="jk2-uV-FdO"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Kkt-hM-ScW">
|
||||
<rect key="frame" x="16" y="43.5" width="288" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="0.5" id="Fkq-bT-IYv"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="Kkt-hM-ScW" secondAttribute="bottom" id="AvY-H1-0YN"/>
|
||||
<constraint firstItem="Kkt-hM-ScW" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="E5g-hz-SLI"/>
|
||||
<constraint firstItem="IXi-sc-YIw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="SRF-Zx-Y0R"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="Kkt-hM-ScW" secondAttribute="trailing" id="YML-R1-ezq"/>
|
||||
<constraint firstItem="IXi-sc-YIw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" id="iD5-Av-ORS"/>
|
||||
<constraint firstAttribute="bottom" secondItem="IXi-sc-YIw" secondAttribute="bottom" id="kpD-6Q-qKi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="eFB-F1-d3A" id="xGo-40-nn7"/>
|
||||
<outlet property="avatarContainerWidthConstraint" destination="tiI-Rj-gjh" id="34n-ev-EKi"/>
|
||||
<outlet property="replyCountLabel" destination="Dcm-ll-GeE" id="E4m-xk-DiQ"/>
|
||||
<outlet property="stackViewLeadingConstraint" destination="iD5-Av-ORS" id="Try-cG-8uA"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="132" y="132"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="separatorColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="tintColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -1,505 +0,0 @@
|
|||
//
|
||||
// ConversationMainStatusCollectionViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 1/20/23.
|
||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import Combine
|
||||
|
||||
class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
|
||||
|
||||
static let contentFont = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 18))
|
||||
static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 18, weight: .regular))
|
||||
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
|
||||
static let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15))
|
||||
|
||||
static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
// MARK: Subviews
|
||||
|
||||
private static let avatarImageViewSize: CGFloat = 50
|
||||
private(set) lazy var avatarImageView = CachedImageView(cache: .avatars).configure {
|
||||
$0.layer.masksToBounds = true
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: ConversationMainStatusCollectionViewCell.avatarImageViewSize),
|
||||
$0.widthAnchor.constraint(equalToConstant: ConversationMainStatusCollectionViewCell.avatarImageViewSize),
|
||||
])
|
||||
$0.isUserInteractionEnabled = true
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||
}
|
||||
|
||||
let displayNameLabel = EmojiLabel().configure {
|
||||
$0.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
||||
private let metaIndicatorsView = StatusMetaIndicatorsView().configure {
|
||||
$0.allowedIndicators = [.visibility, .localOnly]
|
||||
$0.squeezeHorizontal = true
|
||||
$0.primaryAxis = .horizontal
|
||||
}
|
||||
|
||||
let usernameLabel = UILabel().configure {
|
||||
$0.textColor = .secondaryLabel
|
||||
$0.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 17, weight: .light))
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
||||
private lazy var accountDetailContainerView = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
$0.addSubview(avatarImageView)
|
||||
$0.addSubview(displayNameLabel)
|
||||
$0.addSubview(metaIndicatorsView)
|
||||
$0.addSubview(usernameLabel)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
displayNameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
metaIndicatorsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: $0.leadingAnchor),
|
||||
avatarImageView.topAnchor.constraint(equalTo: $0.topAnchor),
|
||||
avatarImageView.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
|
||||
|
||||
displayNameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
|
||||
displayNameLabel.trailingAnchor.constraint(equalTo: metaIndicatorsView.leadingAnchor, constant: -8),
|
||||
displayNameLabel.topAnchor.constraint(equalTo: $0.topAnchor),
|
||||
|
||||
metaIndicatorsView.topAnchor.constraint(equalTo: $0.topAnchor),
|
||||
metaIndicatorsView.trailingAnchor.constraint(equalTo: $0.trailingAnchor),
|
||||
|
||||
usernameLabel.leadingAnchor.constraint(equalTo: displayNameLabel.leadingAnchor),
|
||||
usernameLabel.trailingAnchor.constraint(equalTo: $0.trailingAnchor),
|
||||
usernameLabel.topAnchor.constraint(equalTo: displayNameLabel.bottomAnchor),
|
||||
usernameLabel.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
|
||||
])
|
||||
$0.isUserInteractionEnabled = true
|
||||
$0.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
$0.addInteraction(UIDragInteraction(delegate: self))
|
||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
|
||||
}
|
||||
private lazy var accountDetailAccessibilityElement = ConversationMainStatusAccountDetailAccessibilityElement(accessibilityContainer: self)
|
||||
|
||||
private(set) lazy var contentWarningLabel = EmojiLabel().configure {
|
||||
$0.numberOfLines = 0
|
||||
$0.textColor = .secondaryLabel
|
||||
$0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
|
||||
.traits: [
|
||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue,
|
||||
]
|
||||
]), size: 0)
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
// this needs to have a higher priorty than the content container's zero height constraint
|
||||
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
$0.isUserInteractionEnabled = true
|
||||
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed)))
|
||||
}
|
||||
|
||||
private(set) lazy var collapseButton = StatusCollapseButton(configuration: {
|
||||
var config = UIButton.Configuration.filled()
|
||||
config.image = UIImage(systemName: "chevron.down")
|
||||
return config
|
||||
}()).configure {
|
||||
// this button is so big that dimming its background color is visually distracting
|
||||
$0.tintAdjustmentMode = .normal
|
||||
$0.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
let contentContainer = StatusContentContainer(useTopSpacer: true).configure {
|
||||
$0.contentTextView.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||
$0.contentTextView.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
||||
$0.contentTextView.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
||||
$0.contentTextView.isSelectable = true
|
||||
$0.contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
||||
if #available(iOS 16.0, *) {
|
||||
$0.contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
|
||||
}
|
||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
|
||||
private lazy var favoritesCountButton = UIButton().configure {
|
||||
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||
$0.addTarget(self, action: #selector(favoritesCountPressed), for: .touchUpInside)
|
||||
$0.isPointerInteractionEnabled = true
|
||||
}
|
||||
|
||||
private lazy var reblogsCountButton = UIButton().configure {
|
||||
$0.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||
$0.addTarget(self, action: #selector(reblogsCountPressed), for: .touchUpInside)
|
||||
$0.isPointerInteractionEnabled = true
|
||||
}
|
||||
|
||||
private lazy var actionsCountHStack = UIStackView(arrangedSubviews: [
|
||||
favoritesCountButton,
|
||||
reblogsCountButton,
|
||||
]).configure {
|
||||
$0.axis = .horizontal
|
||||
$0.spacing = 8
|
||||
}
|
||||
|
||||
private let timestampAndClientLabel = UILabel().configure {
|
||||
$0.textColor = .secondaryLabel
|
||||
$0.font = ConversationMainStatusCollectionViewCell.metaFont
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
||||
private let firstSeparator = UIView().configure {
|
||||
$0.backgroundColor = .separator
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
])
|
||||
}
|
||||
private let secondSeparator = UIView().configure {
|
||||
$0.backgroundColor = .separator
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
])
|
||||
}
|
||||
|
||||
private lazy var metaVStack = UIStackView(arrangedSubviews: [
|
||||
firstSeparator,
|
||||
actionsCountHStack,
|
||||
timestampAndClientLabel,
|
||||
secondSeparator,
|
||||
]).configure {
|
||||
$0.axis = .vertical
|
||||
$0.spacing = 4
|
||||
$0.alignment = .leading
|
||||
}
|
||||
|
||||
private(set) lazy var replyButton = UIButton().configure {
|
||||
$0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var favoriteButton = UIButton().configure {
|
||||
$0.setImage(UIImage(systemName: "star.fill"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var reblogButton = UIButton().configure {
|
||||
$0.setImage(UIImage(systemName: "repeat"), for: .normal)
|
||||
$0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private(set) lazy var moreButton = UIButton().configure {
|
||||
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
||||
$0.showsMenuAsPrimaryAction = true
|
||||
$0.addInteraction(UIPointerInteraction(delegate: self))
|
||||
}
|
||||
|
||||
private var actionButtons: [UIButton] {
|
||||
[replyButton, favoriteButton, reblogButton, moreButton]
|
||||
}
|
||||
|
||||
private lazy var actionsHStack = UIStackView(arrangedSubviews: [
|
||||
replyButton,
|
||||
favoriteButton,
|
||||
reblogButton,
|
||||
moreButton,
|
||||
]).configure {
|
||||
$0.axis = .horizontal
|
||||
$0.distribution = .fillEqually
|
||||
NSLayoutConstraint.activate([
|
||||
$0.heightAnchor.constraint(equalToConstant: 26),
|
||||
])
|
||||
}
|
||||
|
||||
private let accountDetailToContentSpacer = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
$0.heightAnchor.constraint(equalToConstant: 8).isActive = true
|
||||
}
|
||||
private let contentWarningToCollapseButtonSpacer = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
$0.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
||||
}
|
||||
private let contentToMetaSpacer = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
$0.heightAnchor.constraint(equalToConstant: 8).isActive = true
|
||||
}
|
||||
private let metaToActionsSpacer = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
$0.heightAnchor.constraint(equalToConstant: 8).isActive = true
|
||||
}
|
||||
|
||||
private lazy var mainVStack = UIStackView(arrangedSubviews: [
|
||||
accountDetailContainerView,
|
||||
accountDetailToContentSpacer,
|
||||
contentWarningLabel,
|
||||
contentWarningToCollapseButtonSpacer,
|
||||
collapseButton,
|
||||
contentContainer,
|
||||
contentToMetaSpacer,
|
||||
metaVStack,
|
||||
metaToActionsSpacer,
|
||||
actionsHStack,
|
||||
]).configure {
|
||||
$0.axis = .vertical
|
||||
$0.spacing = 0
|
||||
$0.alignment = .leading
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
accountDetailContainerView.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
contentWarningLabel.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
collapseButton.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
contentContainer.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
firstSeparator.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
secondSeparator.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
actionsHStack.widthAnchor.constraint(equalTo: $0.widthAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
var prevThreadLinkView: UIView?
|
||||
var nextThreadLinkView: UIView?
|
||||
|
||||
// MARK: Cell State
|
||||
var mastodonController: MastodonController! { delegate?.apiController }
|
||||
weak var delegate: StatusCollectionViewCellDelegate?
|
||||
var showStatusAutomatically = false
|
||||
|
||||
var statusID: String!
|
||||
var statusState: CollapseState!
|
||||
var accountID: String!
|
||||
|
||||
var isGrayscale = false
|
||||
private var hasCreatedObservers = false
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
// don't show selection background, because this cell isn't selectable
|
||||
automaticallyUpdatesBackgroundConfiguration = false
|
||||
|
||||
mainVStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(mainVStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
mainVStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
mainVStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||
mainVStack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||
mainVStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
|
||||
])
|
||||
|
||||
accessibilityElements = [
|
||||
accountDetailAccessibilityElement,
|
||||
contentWarningLabel,
|
||||
collapseButton,
|
||||
contentContainer.contentTextView,
|
||||
contentContainer.attachmentsView,
|
||||
contentContainer.pollView,
|
||||
favoritesCountButton,
|
||||
reblogsCountButton,
|
||||
timestampAndClientLabel,
|
||||
replyButton,
|
||||
favoriteButton,
|
||||
reblogButton,
|
||||
moreButton,
|
||||
]
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: Configure UI
|
||||
|
||||
func updateUI(statusID: String, state: CollapseState) {
|
||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
createObservers()
|
||||
|
||||
self.statusID = statusID
|
||||
self.statusState = state
|
||||
|
||||
doUpdateUI(status: status)
|
||||
|
||||
accountDetailAccessibilityElement.navigationDelegate = delegate
|
||||
accountDetailAccessibilityElement.accountID = accountID
|
||||
|
||||
var timestampAndClientText = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: status.createdAt)
|
||||
if let application = status.applicationName {
|
||||
timestampAndClientText += " • \(application)"
|
||||
}
|
||||
timestampAndClientLabel.text = timestampAndClientText
|
||||
}
|
||||
|
||||
private func createObservers() {
|
||||
guard !hasCreatedObservers else {
|
||||
return
|
||||
}
|
||||
hasCreatedObservers = true
|
||||
baseCreateObservers()
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
baseUpdateStatusState(status: status)
|
||||
|
||||
let attributes = AttributeContainer([
|
||||
.font: ConversationMainStatusCollectionViewCell.metaFont
|
||||
])
|
||||
|
||||
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
||||
var favoritesConfig = UIButton.Configuration.plain()
|
||||
favoritesConfig.baseForegroundColor = .secondaryLabel
|
||||
favoritesConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), attributes: attributes)
|
||||
favoritesConfig.contentInsets = .zero
|
||||
favoritesCountButton.configuration = favoritesConfig
|
||||
|
||||
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
||||
var reblogsConfig = UIButton.Configuration.plain()
|
||||
reblogsConfig.baseForegroundColor = .secondaryLabel
|
||||
reblogsConfig.attributedTitle = AttributedString(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), attributes: attributes)
|
||||
reblogsConfig.contentInsets = .zero
|
||||
reblogsCountButton.configuration = reblogsConfig
|
||||
}
|
||||
|
||||
func updateUIForPreferences(status: StatusMO) {
|
||||
baseUpdateUIForPreferences(status: status)
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
guard let mastodonController,
|
||||
let statusID,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
return
|
||||
}
|
||||
|
||||
updateUIForPreferences(status: status)
|
||||
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
updateGrayscaleableUI(status: status)
|
||||
}
|
||||
|
||||
if actionsCountHStack.isHidden != !Preferences.shared.showFavoriteAndReblogCounts {
|
||||
actionsCountHStack.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
|
||||
delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
||||
@objc private func accountPressed() {
|
||||
delegate?.selected(account: accountID)
|
||||
}
|
||||
|
||||
@objc private func collapseButtonPressed() {
|
||||
toggleCollapse()
|
||||
}
|
||||
|
||||
@objc private func favoritesCountPressed() {
|
||||
guard let delegate else {
|
||||
return
|
||||
}
|
||||
let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
||||
// TODO: only show warning if the instance isn't the logged in one
|
||||
vc.showInacurateCountWarning = true
|
||||
delegate.show(vc)
|
||||
}
|
||||
|
||||
@objc private func reblogsCountPressed() {
|
||||
guard let delegate else {
|
||||
return
|
||||
}
|
||||
let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
||||
vc.showInacurateCountWarning = true
|
||||
delegate.show(vc)
|
||||
}
|
||||
|
||||
@objc private func replyPressed() {
|
||||
delegate?.compose(inReplyToID: statusID)
|
||||
}
|
||||
|
||||
@objc private func favoritePressed() {
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
@objc private func reblogPressed() {
|
||||
toggleReblog()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ConversationMainStatusAccountDetailAccessibilityElement: UIAccessibilityElement {
|
||||
var navigationDelegate: TuskerNavigationDelegate!
|
||||
var mastodonController: MastodonController { navigationDelegate.apiController }
|
||||
var accountID: String!
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get { mastodonController.persistentContainer.account(for: accountID)?.displayNameWithoutCustomEmoji }
|
||||
set {}
|
||||
}
|
||||
|
||||
override var accessibilityHint: String? {
|
||||
get { "Double tap to show profile." }
|
||||
set {}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
navigationDelegate.selected(account: accountID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationMainStatusCollectionViewCell: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return contextMenuConfigurationForAccount(sourceView: accountDetailContainerView)
|
||||
}
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
if let delegate {
|
||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: delegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationMainStatusCollectionViewCell: UIDragInteractionDelegate {
|
||||
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
|
||||
return dragItemsForAccount()
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationMainStatusCollectionViewCell: UIPointerInteractionDelegate {
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
|
||||
if interaction.view == avatarImageView {
|
||||
return defaultRegion
|
||||
} else if let button = interaction.view as? UIButton,
|
||||
actionButtons.contains(button) {
|
||||
var rect = button.convert(button.imageView!.bounds, to: button.imageView!)
|
||||
rect = rect.insetBy(dx: -24, dy: -24)
|
||||
return UIPointerRegion(rect: rect)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||
if interaction.view == avatarImageView {
|
||||
let preview = UITargetedPreview(view: avatarImageView)
|
||||
return UIPointerStyle(effect: .lift(preview))
|
||||
} else if let button = interaction.view as? UIButton,
|
||||
actionButtons.contains(button) {
|
||||
let preview = UITargetedPreview(view: button.imageView!)
|
||||
var rect = button.convert(button.imageView!.bounds, to: button.imageView!)
|
||||
rect = rect.insetBy(dx: -24, dy: -24)
|
||||
return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
//
|
||||
// ConversationMainStatusTableViewCell.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/28/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import Pachyderm
|
||||
|
||||
class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
|
||||
|
||||
static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
|
||||
|
||||
@IBOutlet weak var profileDetailContainerView: UIView!
|
||||
@IBOutlet weak var favoriteAndReblogCountStackView: UIStackView!
|
||||
@IBOutlet weak var totalFavoritesButton: UIButton!
|
||||
@IBOutlet weak var totalReblogsButton: UIButton!
|
||||
@IBOutlet weak var timestampAndClientLabel: UILabel!
|
||||
|
||||
private var profileAccessibilityElement: ConversationMainStatusProfileAccessibilityElement!
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
profileAccessibilityElement = ConversationMainStatusProfileAccessibilityElement(accessibilityContainer: self)
|
||||
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
|
||||
accessibilityElements = [
|
||||
profileAccessibilityElement!,
|
||||
contentWarningLabel!,
|
||||
collapseButton!,
|
||||
contentTextView!,
|
||||
attachmentsView!,
|
||||
pollView!,
|
||||
totalFavoritesButton!,
|
||||
totalReblogsButton!,
|
||||
timestampAndClientLabel!,
|
||||
replyButton!,
|
||||
favoriteButton!,
|
||||
reblogButton!,
|
||||
moreButton!,
|
||||
]
|
||||
|
||||
profileDetailContainerView.addInteraction(UIContextMenuInteraction(delegate: self))
|
||||
|
||||
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 24, weight: .semibold))
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
usernameLabel.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 17, weight: .light))
|
||||
usernameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
metaIndicatorsView.allowedIndicators = [.visibility, .localOnly]
|
||||
metaIndicatorsView.squeezeHorizontal = true
|
||||
metaIndicatorsView.primaryAxis = .horizontal
|
||||
|
||||
contentWarningLabel.font = .preferredFont(forTextStyle: .body).withTraits(.traitBold)!
|
||||
contentWarningLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
contentTextView.defaultFont = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 18))
|
||||
contentTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 18, weight: .regular))
|
||||
contentTextView.adjustsFontForContentSizeCategory = true
|
||||
contentTextView.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
||||
if #available(iOS 16.0, *) {
|
||||
contentTextView.dataDetectorTypes.formUnion([.money, .physicalValue])
|
||||
}
|
||||
|
||||
let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15))
|
||||
totalFavoritesButton.titleLabel!.font = metaFont
|
||||
totalFavoritesButton.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||
totalReblogsButton.titleLabel!.font = metaFont
|
||||
totalReblogsButton.titleLabel!.adjustsFontForContentSizeCategory = true
|
||||
timestampAndClientLabel.font = metaFont
|
||||
timestampAndClientLabel.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
|
||||
override func doUpdateUI(status: StatusMO, state: CollapseState) {
|
||||
super.doUpdateUI(status: status, state: state)
|
||||
|
||||
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
|
||||
if let application = status.applicationName {
|
||||
timestampAndClientText += " • \(application)"
|
||||
}
|
||||
timestampAndClientLabel.text = timestampAndClientText
|
||||
}
|
||||
|
||||
override func updateStatusState(status: StatusMO) {
|
||||
super.updateStatusState(status: status)
|
||||
|
||||
let favoritesFormat = NSLocalizedString("favorites count", comment: "conv main status favorites button label")
|
||||
totalFavoritesButton.setTitle(String.localizedStringWithFormat(favoritesFormat, status.favouritesCount), for: .normal)
|
||||
let reblogsFormat = NSLocalizedString("reblogs count", comment: "conv main status reblogs button label")
|
||||
totalReblogsButton.setTitle(String.localizedStringWithFormat(reblogsFormat, status.reblogsCount), for: .normal)
|
||||
}
|
||||
|
||||
override func updateUI(account: AccountMO) {
|
||||
super.updateUI(account: account)
|
||||
profileAccessibilityElement.navigationDelegate = delegate
|
||||
profileAccessibilityElement.accountID = account.id
|
||||
}
|
||||
|
||||
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
||||
super.updateUIForPreferences(account: account, status: status)
|
||||
|
||||
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
|
||||
}
|
||||
|
||||
@IBAction func totalFavoritesPressed() {
|
||||
if let delegate = delegate {
|
||||
// accounts aren't known, pass nil so the VC will load them
|
||||
let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
||||
vc.showInacurateCountWarning = true
|
||||
delegate.show(vc)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func totalReblogsPressed() {
|
||||
if let delegate = delegate {
|
||||
// accounts aren't known, pass nil so the VC will load them
|
||||
let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, statusState: statusState.copy(), accountIDs: nil)
|
||||
vc.showInacurateCountWarning = true
|
||||
delegate.show(vc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConversationMainStatusProfileAccessibilityElement: UIAccessibilityElement {
|
||||
var navigationDelegate: TuskerNavigationDelegate!
|
||||
var mastodonController: MastodonController { navigationDelegate.apiController }
|
||||
var accountID: String!
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get { mastodonController.persistentContainer.account(for: accountID)?.displayNameWithoutCustomEmoji }
|
||||
set {}
|
||||
}
|
||||
|
||||
override var accessibilityHint: String? {
|
||||
get { "Double tap to show profile." }
|
||||
set {}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
navigationDelegate.selected(account: accountID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension ConversationMainStatusTableViewCell: UIContextMenuInteractionDelegate {
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return UIContextMenuConfiguration(identifier: nil) {
|
||||
ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
|
||||
} actionProvider: { (_) in
|
||||
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(self.avatarImageView)) ?? [])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="Image references" minToolsVersion="12.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 clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="304" id="IDI-ur-8pa" customClass="ConversationMainStatusTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="304"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="IDI-ur-8pa" id="MkV-Jo-zuv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="304"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="GuG-Qd-B8I">
|
||||
<rect key="frame" x="16" y="8" width="361" height="288"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="Cnd-Fj-B7l">
|
||||
<rect key="frame" x="0.0" y="0.0" width="361" height="50"/>
|
||||
<subviews>
|
||||
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="mB9-HO-1vf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="XPF-UL-68q"/>
|
||||
<constraint firstAttribute="width" constant="50" id="Yxp-Vr-dfl"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="249" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="12" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lZY-2e-17d" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="58" y="0.0" width="146.5" height="29"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SWg-Ka-QyP">
|
||||
<rect key="frame" x="58" y="29" width="303" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xD6-dy-0XV" customClass="StatusMetaIndicatorsView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="212.5" y="0.0" width="148.5" height="22"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="22" placeholder="YES" id="wF5-Ii-LO5"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="SWg-Ka-QyP" secondAttribute="trailing" id="4g6-BT-eW4"/>
|
||||
<constraint firstItem="xD6-dy-0XV" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="7Io-sX-c9k"/>
|
||||
<constraint firstItem="lZY-2e-17d" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="8fU-y9-K5Z"/>
|
||||
<constraint firstItem="lZY-2e-17d" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="Aqj-co-Szp"/>
|
||||
<constraint firstItem="xD6-dy-0XV" firstAttribute="leading" secondItem="lZY-2e-17d" secondAttribute="trailing" constant="8" id="PfV-YZ-k9j"/>
|
||||
<constraint firstItem="mB9-HO-1vf" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="R7P-rD-Gbm"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mB9-HO-1vf" secondAttribute="bottom" id="Wd0-Qh-idS"/>
|
||||
<constraint firstItem="mB9-HO-1vf" firstAttribute="leading" secondItem="Cnd-Fj-B7l" secondAttribute="leading" id="bxq-Fs-1aH"/>
|
||||
<constraint firstItem="SWg-Ka-QyP" firstAttribute="leading" secondItem="mB9-HO-1vf" secondAttribute="trailing" constant="8" id="e45-gE-myI"/>
|
||||
<constraint firstItem="SWg-Ka-QyP" firstAttribute="top" secondItem="lZY-2e-17d" secondAttribute="bottom" id="lvX-1b-8cN"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xD6-dy-0XV" secondAttribute="trailing" id="tfq-dR-UT7"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="58" width="361" height="20.5"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="86.5" width="361" height="30"/>
|
||||
<color key="backgroundColor" systemColor="tintColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="icD-3q-uJ6"/>
|
||||
</constraints>
|
||||
<color key="tintColor" systemColor="tintColor"/>
|
||||
<state key="normal" image="chevron.down" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
|
||||
</state>
|
||||
<buttonConfiguration key="configuration" style="filled" image="chevron.down" catalog="system">
|
||||
<preferredSymbolConfiguration key="preferredSymbolConfigurationForImage" scale="large"/>
|
||||
<color key="baseForegroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="collapseButtonPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="00b-nM-U5g"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z0g-HN-gS0" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="124.5" width="361" height="2.5"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="20"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="131" width="361" height="0.0"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" priority="999" constant="90" id="Tdo-Hv-ITE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="131" width="361" height="0.0"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TLv-Xu-tT1" customClass="StatusPollView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="135" width="361" height="50"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ejU-sO-Og5">
|
||||
<rect key="frame" x="0.0" y="193" width="361" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="0.5" id="DRI-lB-TyG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="HZv-qj-gi6">
|
||||
<rect key="frame" x="0.0" y="201.5" width="142" height="18"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yyj-Bs-Vjq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="75" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="18" id="F9W-LW-swd"/>
|
||||
</constraints>
|
||||
<state key="normal" title="2 Favorites">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="totalFavoritesPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="QEe-yA-n91"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
|
||||
<rect key="frame" x="83" y="0.0" width="59" height="18"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="18" id="k0P-W7-wMF"/>
|
||||
</constraints>
|
||||
<state key="normal" title="1 Reblog">
|
||||
<color key="titleColor" systemColor="secondaryLabelColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="totalReblogsPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="Rmo-Mm-z1A"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Sep 7, 2019 12:12:53 PM • Web" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YHN-wG-YWi">
|
||||
<rect key="frame" x="0.0" y="227.5" width="213.5" height="18"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||
</accessibility>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3Fp-Nj-sVj">
|
||||
<rect key="frame" x="0.0" y="253.5" width="361" height="0.5"/>
|
||||
<color key="backgroundColor" systemColor="opaqueSeparatorColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="0.5" id="akf-Kl-8mK"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="3Bg-XP-d13">
|
||||
<rect key="frame" x="0.0" y="262" width="361" height="26"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2cc-lE-AdG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="90.5" height="26"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reply"/>
|
||||
<color key="tintColor" systemColor="tintColor"/>
|
||||
<state key="normal">
|
||||
<imageReference key="image" image="arrowshape.turn.up.left.fill" catalog="system" symbolScale="large"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="replyPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="Wic-n9-0Tt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA">
|
||||
<rect key="frame" x="90.5" y="0.0" width="90" height="26"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Favorite"/>
|
||||
<color key="tintColor" systemColor="tintColor"/>
|
||||
<state key="normal">
|
||||
<imageReference key="image" image="star.fill" catalog="system" symbolScale="large"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="favoritePressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="GjO-Fw-3tF"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy">
|
||||
<rect key="frame" x="180.5" y="0.0" width="90.5" height="26"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Reblog"/>
|
||||
<color key="tintColor" systemColor="tintColor"/>
|
||||
<state key="normal">
|
||||
<imageReference key="image" image="repeat" catalog="system" symbolScale="large"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="reblogPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="iLg-O3-i33"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" pointerInteraction="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK">
|
||||
<rect key="frame" x="271" y="0.0" width="90" height="26"/>
|
||||
<accessibility key="accessibilityConfiguration" label="More Actions"/>
|
||||
<color key="tintColor" systemColor="tintColor"/>
|
||||
<state key="normal">
|
||||
<imageReference key="image" image="ellipsis" catalog="system" symbolScale="large"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="morePressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="JNt-fh-WYW"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="26" id="bqe-m8-5Lo"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="QqC-GR-TLC" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2WL-jD-I09"/>
|
||||
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
|
||||
<constraint firstItem="z0g-HN-gS0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="4TF-2Z-mdf"/>
|
||||
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
|
||||
<constraint firstItem="cwQ-mR-L1b" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="O32-3Q-mUs"/>
|
||||
<constraint firstItem="8r8-O8-Agh" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="bZv-bR-jJ3"/>
|
||||
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
||||
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
||||
<constraint firstItem="3Fp-Nj-sVj" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="kfI-WN-ouW"/>
|
||||
<constraint firstItem="TLv-Xu-tT1" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="v87-hd-fd4"/>
|
||||
</constraints>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="GuG-Qd-B8I" secondAttribute="trailing" id="GP2-Xt-d67"/>
|
||||
<constraint firstAttribute="bottom" secondItem="GuG-Qd-B8I" secondAttribute="bottom" constant="8" id="Il3-lH-xux"/>
|
||||
<constraint firstItem="GuG-Qd-B8I" firstAttribute="leading" secondItem="MkV-Jo-zuv" secondAttribute="leadingMargin" id="irk-Qi-Wqw"/>
|
||||
<constraint firstItem="GuG-Qd-B8I" firstAttribute="top" secondItem="MkV-Jo-zuv" secondAttribute="top" constant="8" id="w4H-TR-7gK"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="attachmentsView" destination="IF9-9U-Gk0" id="UeF-4a-G6k"/>
|
||||
<outlet property="avatarImageView" destination="mB9-HO-1vf" id="GAt-8i-MIE"/>
|
||||
<outlet property="cardView" destination="QqC-GR-TLC" id="gkB-5d-ute"/>
|
||||
<outlet property="collapseButton" destination="8r8-O8-Agh" id="kLb-x0-Qx9"/>
|
||||
<outlet property="contentTextView" destination="z0g-HN-gS0" id="ILZ-WW-zbU"/>
|
||||
<outlet property="contentWarningLabel" destination="cwQ-mR-L1b" id="JN1-VC-Fql"/>
|
||||
<outlet property="displayNameLabel" destination="lZY-2e-17d" id="FFF-kc-1q2"/>
|
||||
<outlet property="favoriteAndReblogCountStackView" destination="HZv-qj-gi6" id="KGA-as-022"/>
|
||||
<outlet property="favoriteButton" destination="DhN-rJ-jdA" id="f6b-VR-dsA"/>
|
||||
<outlet property="metaIndicatorsView" destination="xD6-dy-0XV" id="sx9-Pf-Qox"/>
|
||||
<outlet property="moreButton" destination="Ujo-Ap-dmK" id="tBm-jm-2FR"/>
|
||||
<outlet property="pollView" destination="TLv-Xu-tT1" id="l9J-Tv-ndf"/>
|
||||
<outlet property="profileDetailContainerView" destination="Cnd-Fj-B7l" id="32j-B2-ueG"/>
|
||||
<outlet property="reblogButton" destination="GUG-f7-Hdy" id="ZRh-Qy-HXG"/>
|
||||
<outlet property="replyButton" destination="2cc-lE-AdG" id="EBZ-RJ-Qbp"/>
|
||||
<outlet property="timestampAndClientLabel" destination="YHN-wG-YWi" id="ewe-Ad-dYR"/>
|
||||
<outlet property="totalFavoritesButton" destination="yyj-Bs-Vjq" id="m4k-RB-lM0"/>
|
||||
<outlet property="totalReblogsButton" destination="dem-vG-cPB" id="AHj-6A-5qm"/>
|
||||
<outlet property="usernameLabel" destination="SWg-Ka-QyP" id="OFL-Sc-OQX"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="-631.20000000000005" y="-109.74512743628186"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrowshape.turn.up.left.fill" catalog="system" width="128" height="104"/>
|
||||
<image name="chevron.down" catalog="system" width="128" height="70"/>
|
||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||
<image name="repeat" catalog="system" width="128" height="98"/>
|
||||
<image name="star.fill" catalog="system" width="128" height="116"/>
|
||||
<systemColor name="labelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="opaqueSeparatorColor">
|
||||
<color red="0.77647058823529413" green="0.77647058823529413" blue="0.78431372549019607" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="tintColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -29,13 +29,12 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
|||
var favoriteButton: UIButton { get }
|
||||
var reblogButton: UIButton { get }
|
||||
var moreButton: UIButton { get }
|
||||
var prevThreadLinkView: UIView? { get set }
|
||||
var nextThreadLinkView: UIView? { get set }
|
||||
|
||||
var delegate: StatusCollectionViewCellDelegate? { get }
|
||||
var mastodonController: MastodonController! { get }
|
||||
|
||||
var showStatusAutomatically: Bool { get }
|
||||
var showReplyIndicator: Bool { get }
|
||||
|
||||
var statusID: String! { get set }
|
||||
var statusState: CollapseState! { get set }
|
||||
|
@ -45,7 +44,6 @@ protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate
|
|||
var cancellables: Set<AnyCancellable> { get set }
|
||||
|
||||
func updateUIForPreferences(status: StatusMO)
|
||||
func updateStatusState(status: StatusMO)
|
||||
}
|
||||
|
||||
// MARK: UI Configuration
|
||||
|
@ -181,7 +179,7 @@ extension StatusCollectionViewCell {
|
|||
displayNameLabel.updateForAccountDisplayName(account: status.account)
|
||||
}
|
||||
|
||||
func baseUpdateStatusState(status: StatusMO) {
|
||||
func updateStatusState(status: StatusMO) {
|
||||
if status.favourited {
|
||||
favoriteButton.tintColor = UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)
|
||||
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
|
||||
|
@ -206,55 +204,6 @@ extension StatusCollectionViewCell {
|
|||
contentContainer.pollView.toastableViewController = delegate?.toastableViewController
|
||||
contentContainer.pollView.updateUI(status: status, poll: status.poll)
|
||||
}
|
||||
|
||||
func setShowThreadLinks(prev: Bool, next: Bool) {
|
||||
if prev {
|
||||
if let prevThreadLinkView {
|
||||
prevThreadLinkView.isHidden = false
|
||||
} else {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = .tintColor.withAlphaComponent(0.5)
|
||||
view.layer.cornerRadius = 2.5
|
||||
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
prevThreadLinkView = view
|
||||
contentView.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 5),
|
||||
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
prevThreadLinkView?.isHidden = true
|
||||
}
|
||||
|
||||
if next {
|
||||
if let nextThreadLinkView {
|
||||
nextThreadLinkView.isHidden = false
|
||||
} else {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = .tintColor.withAlphaComponent(0.5)
|
||||
view.layer.cornerRadius = 2.5
|
||||
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||
nextThreadLinkView = view
|
||||
contentView.addSubview(view)
|
||||
let bottomConstraint = view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
|
||||
// let this constraint get broken during intermediate layouts to avoid a bunch of spurious 'unable to simultaneously satisfy constraints' messages
|
||||
bottomConstraint.priority = .init(999)
|
||||
NSLayoutConstraint.activate([
|
||||
view.widthAnchor.constraint(equalToConstant: 5),
|
||||
view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 2),
|
||||
bottomConstraint,
|
||||
])
|
||||
}
|
||||
} else {
|
||||
nextThreadLinkView?.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
|
|
@ -9,15 +9,8 @@
|
|||
import UIKit
|
||||
|
||||
class StatusContentContainer: UIView {
|
||||
|
||||
private var useTopSpacer = false
|
||||
private let topSpacer = UIView().configure {
|
||||
$0.backgroundColor = .clear
|
||||
// other 4pt is provided by this view's own spacing
|
||||
$0.heightAnchor.constraint(equalToConstant: 4).isActive = true
|
||||
}
|
||||
|
||||
let contentTextView = StatusContentTextView().configure {
|
||||
let contentTextView = StatusContentTextView().configure {
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
$0.isScrollEnabled = false
|
||||
$0.backgroundColor = nil
|
||||
|
@ -36,11 +29,7 @@ class StatusContentContainer: UIView {
|
|||
let pollView = StatusPollView()
|
||||
|
||||
private var arrangedSubviews: [UIView] {
|
||||
if useTopSpacer {
|
||||
return [topSpacer, contentTextView, cardView, attachmentsView, pollView]
|
||||
} else {
|
||||
return [contentTextView, cardView, attachmentsView, pollView]
|
||||
}
|
||||
[contentTextView, cardView, attachmentsView, pollView]
|
||||
}
|
||||
|
||||
private var isHiddenObservations: [NSKeyValueObservation] = []
|
||||
|
@ -55,10 +44,8 @@ class StatusContentContainer: UIView {
|
|||
subviews.filter { !$0.isHidden }.map(\.bounds.height).reduce(0, +)
|
||||
}
|
||||
|
||||
init(useTopSpacer: Bool) {
|
||||
self.useTopSpacer = useTopSpacer
|
||||
|
||||
super.init(frame: .zero)
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
for subview in arrangedSubviews {
|
||||
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -166,7 +166,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
$0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
let contentContainer = StatusContentContainer(useTopSpacer: false).configure {
|
||||
let contentContainer = StatusContentContainer().configure {
|
||||
$0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||
$0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
|
@ -260,9 +260,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
var prevThreadLinkView: UIView?
|
||||
var nextThreadLinkView: UIView?
|
||||
|
||||
// MARK: Cell state
|
||||
|
||||
private var mainContainerTopToReblogLabelConstraint: NSLayoutConstraint!
|
||||
|
@ -273,8 +270,14 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
weak var overrideMastodonController: MastodonController?
|
||||
var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController }
|
||||
weak var delegate: StatusCollectionViewCellDelegate?
|
||||
var showStatusAutomatically = false
|
||||
var showReplyIndicator = true
|
||||
var showStatusAutomatically: Bool {
|
||||
// TODO: needed once conversation controller refactored
|
||||
false
|
||||
}
|
||||
var showReplyIndicator: Bool {
|
||||
// TODO: needed once conversation controller refactored
|
||||
true
|
||||
}
|
||||
var showPinned: Bool = false
|
||||
var showFollowedHashtags: Bool = false
|
||||
|
||||
|
@ -571,7 +574,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
pinImageView.isHidden = !showPinned
|
||||
}
|
||||
|
||||
private func createObservers() {
|
||||
func createObservers() {
|
||||
guard !hasCreatedObservers else {
|
||||
return
|
||||
}
|
||||
|
@ -606,10 +609,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
}
|
||||
}
|
||||
|
||||
func updateStatusState(status: StatusMO) {
|
||||
baseUpdateStatusState(status: status)
|
||||
}
|
||||
|
||||
private func updateTimestamp() {
|
||||
guard let mastodonController,
|
||||
let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||
|
|
Loading…
Reference in New Issue