Unify most of TimelineStatus and ConverastionMainStatus cell code

Closes #54
This commit is contained in:
Shadowfacts 2019-11-19 12:08:11 -05:00
parent 8fb3b211b6
commit 24a1e7ceb9
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
13 changed files with 350 additions and 576 deletions

View File

@ -28,8 +28,8 @@
D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D60A548D21ED515800F1F87C /* GMImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; }; D60A548F21ED515800F1F87C /* GMImagePicker.h in Headers */ = {isa = PBXBuildFile; fileRef = D60A548D21ED515800F1F87C /* GMImagePicker.h */; settings = {ATTRIBUTES = (Public, ); }; };
D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; }; D60A549221ED515800F1F87C /* GMImagePicker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; };
D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D60A549321ED515800F1F87C /* GMImagePicker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D60A548B21ED515800F1F87C /* GMImagePicker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D60BAFB82383921D00EED893 /* StatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60BAFB72383921D00EED893 /* StatusCell.swift */; };
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; }; D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; };
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; }; D61099BD2144B0CC00432DC2 /* Pachyderm.h in Headers */ = {isa = PBXBuildFile; fileRef = D61099AD2144B0CC00432DC2 /* Pachyderm.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -112,7 +112,7 @@
D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */; }; D66362752137068A00C9CBA2 /* Visibility+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */; };
D667383C23299340000A2373 /* InstanceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667383B23299340000A2373 /* InstanceType.swift */; }; D667383C23299340000A2373 /* InstanceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667383B23299340000A2373 /* InstanceType.swift */; };
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* StatusTableViewCell.xib */; }; D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; }; D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; };
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; }; D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; };
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; }; D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; };
@ -180,7 +180,7 @@
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; }; D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */; };
D6BC9DDA232D8BE5002CA326 /* SearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */; }; D6BC9DDA232D8BE5002CA326 /* SearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */; };
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; };
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; };
D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */; }; D6C693F92162E4DB007D6A6D /* StatusContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */; };
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; };
@ -288,8 +288,8 @@
D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D60A548B21ED515800F1F87C /* GMImagePicker.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GMImagePicker.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = "<group>"; }; D60A548D21ED515800F1F87C /* GMImagePicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GMImagePicker.h; sourceTree = "<group>"; };
D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D60A548E21ED515800F1F87C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D60BAFB72383921D00EED893 /* StatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCell.swift; sourceTree = "<group>"; };
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = "<group>"; }; D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = "<group>"; };
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; }; D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D61099AE2144B0CC00432DC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -372,7 +372,7 @@
D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Visibility+Helpers.swift"; sourceTree = "<group>"; }; D66362742137068A00C9CBA2 /* Visibility+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Visibility+Helpers.swift"; sourceTree = "<group>"; };
D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = "<group>"; }; D667383B23299340000A2373 /* InstanceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceType.swift; sourceTree = "<group>"; };
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = "<group>"; }; D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.xib; sourceTree = "<group>"; };
D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = "<group>"; }; D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTableViewController.swift; sourceTree = "<group>"; };
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = "<group>"; }; D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderTableViewCell.xib; sourceTree = "<group>"; };
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = "<group>"; }; D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderTableViewCell.swift; sourceTree = "<group>"; };
@ -439,7 +439,7 @@
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; }; D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTableViewController.swift; sourceTree = "<group>"; }; D6BC9DD9232D8BE5002CA326 /* SearchTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTableViewController.swift; sourceTree = "<group>"; };
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = "<group>"; };
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = "<group>"; };
D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; }; D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentLabel.swift; sourceTree = "<group>"; };
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = "<group>"; };
@ -829,11 +829,11 @@
D641C78A213DD926004B4513 /* Status */ = { D641C78A213DD926004B4513 /* Status */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D667E5E02134937B0057A976 /* StatusTableViewCell.xib */, D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */,
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */, D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */,
D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */,
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */, D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */, D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
D60BAFB72383921D00EED893 /* StatusCell.swift */,
); );
path = Status; path = Status;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1459,7 +1459,7 @@
0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */, 0411610122B442870030A9B7 /* AttachmentViewController.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */, D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */,
D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -1579,6 +1579,7 @@
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */,
D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */, D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */, D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
@ -1600,11 +1601,10 @@
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */, D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */, D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */, D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
D60BAFB82383921D00EED893 /* StatusCell.swift in Sources */,
D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */, D6AEBB4523216AF800E5038B /* FollowAccountActivity.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */, D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,

View File

@ -43,7 +43,7 @@ class ConversationTableViewController: EnhancedTableViewController {
tableView.delegate = self tableView.delegate = self
tableView.dataSource = self tableView.dataSource = self
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell") tableView.register(UINib(nibName: "ConversationMainStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "mainStatusCell")
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
@ -103,7 +103,7 @@ class ConversationTableViewController: EnhancedTableViewController {
cell.delegate = self cell.delegate = self
return cell return cell
} else { } else {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.updateUI(statusID: statusID) cell.updateUI(statusID: statusID)
cell.delegate = self cell.delegate = self
@ -132,7 +132,7 @@ class ConversationTableViewController: EnhancedTableViewController {
showStatusesAutomatically = !showStatusesAutomatically showStatusesAutomatically = !showStatusesAutomatically
for cell in tableView.visibleCells { for cell in tableView.visibleCells {
guard var cell = cell as? UITableViewCell & StatusCell, guard let cell = cell as? BaseStatusTableViewCell,
cell.collapsible else { continue } cell.collapsible else { continue }
cell.showStatusAutomatically = showStatusesAutomatically cell.showStatusAutomatically = showStatusesAutomatically
cell.setCollapsed(!showStatusesAutomatically, animated: false) cell.setCollapsed(!showStatusesAutomatically, animated: false)

View File

@ -48,7 +48,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: statusCell) tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: actionGroupCell) tableView.register(UINib(nibName: "ActionNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: actionGroupCell)
tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: followGroupCell) tableView.register(UINib(nibName: "FollowNotificationGroupTableViewCell", bundle: nil), forCellReuseIdentifier: followGroupCell)
@ -88,7 +88,7 @@ class NotificationsTableViewController: EnhancedTableViewController {
switch group.kind { switch group.kind {
case .mention: case .mention:
guard let notification = MastodonCache.notification(for: group.notificationIDs.first!), guard let notification = MastodonCache.notification(for: group.notificationIDs.first!),
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? StatusTableViewCell else { let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else {
fatalError() fatalError()
} }
cell.updateUI(statusID: notification.status!.id) cell.updateUI(statusID: notification.status!.id)

View File

@ -63,7 +63,7 @@ class ProfileTableViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell") tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell")
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
@ -165,14 +165,14 @@ class ProfileTableViewController: EnhancedTableViewController {
cell.updateUI(for: accountID) cell.updateUI(for: accountID)
return cell return cell
case 1: case 1:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let statusID = pinnedStatusIDs[indexPath.row] let statusID = pinnedStatusIDs[indexPath.row]
cell.showPinned = true cell.showPinned = true
cell.updateUI(statusID: statusID) cell.updateUI(statusID: statusID)
cell.delegate = self cell.delegate = self
return cell return cell
default: default:
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
let statusID = timelineSegments[indexPath.section - 2][indexPath.row] let statusID = timelineSegments[indexPath.section - 2][indexPath.row]
cell.updateUI(statusID: statusID) cell.updateUI(statusID: statusID)
cell.delegate = self cell.delegate = self

View File

@ -39,7 +39,7 @@ class SearchTableViewController: EnhancedTableViewController {
super.viewDidLoad() super.viewDidLoad()
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell) tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell) tableView.register(UINib(nibName: "HashtagTableViewCell", bundle: .main), forCellReuseIdentifier: hashtagCell)
dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in
@ -55,7 +55,7 @@ class SearchTableViewController: EnhancedTableViewController {
cell.delegate = self cell.delegate = self
return cell return cell
case let .status(id): case let .status(id):
let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! StatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as! TimelineStatusTableViewCell
cell.updateUI(statusID: id) cell.updateUI(statusID: id)
cell.delegate = self cell.delegate = self
return cell return cell

View File

@ -54,7 +54,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell) tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: statusCell)
tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell) tableView.register(UINib(nibName: "AccountTableViewCell", bundle: .main), forCellReuseIdentifier: accountCell)
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
@ -108,7 +108,7 @@ class StatusActionAccountListTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section { switch indexPath.section {
case 0: case 0:
guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? StatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: statusCell, for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.updateUI(statusID: statusID) cell.updateUI(statusID: statusID)
cell.delegate = self cell.delegate = self
return cell return cell

View File

@ -65,7 +65,7 @@ class TimelineTableViewController: EnhancedTableViewController {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 140 tableView.estimatedRowHeight = 140
tableView.register(UINib(nibName: "StatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
tableView.prefetchDataSource = self tableView.prefetchDataSource = self
@ -92,7 +92,7 @@ class TimelineTableViewController: EnhancedTableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? StatusTableViewCell else { fatalError() } guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
cell.updateUI(statusID: statusID(for: indexPath)) cell.updateUI(statusID: statusID(for: indexPath))
cell.delegate = self cell.delegate = self

View File

@ -1,27 +1,20 @@
// //
// StatusTableViewCell.swift // BaseStatusTableViewCell.swift
// Tusker // Tusker
// //
// Created by Shadowfacts on 8/16/18. // Created by Shadowfacts on 11/19/19.
// Copyright © 2018 Shadowfacts. All rights reserved. // Copyright © 2019 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import Combine
import Pachyderm import Pachyderm
import Combine
protocol StatusTableViewCellDelegate: TuskerNavigationDelegate { protocol StatusTableViewCellDelegate: TuskerNavigationDelegate {
func statusCollapsedStateChanged() func statusCollapsedStateChanged()
} }
class StatusTableViewCell: UITableViewCell, StatusCell { class BaseStatusTableViewCell: UITableViewCell {
static let relativeDateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric
formatter.unitsStyle = .short
return formatter
}()
var delegate: StatusTableViewCellDelegate? { var delegate: StatusTableViewCellDelegate? {
didSet { didSet {
@ -29,37 +22,31 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel! @IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentWarningLabel: UILabel! @IBOutlet weak var contentWarningLabel: UILabel!
@IBOutlet weak var collapseButton: UIButton! @IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentLabel: StatusContentLabel! @IBOutlet weak var contentLabel: StatusContentLabel!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var reblogLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var attachmentsView: AttachmentsContainerView! @IBOutlet weak var attachmentsView: AttachmentsContainerView!
@IBOutlet weak var replyButton: UIButton! @IBOutlet weak var replyButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton! @IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var reblogButton: UIButton! @IBOutlet weak var reblogButton: UIButton!
@IBOutlet weak var moreButton: UIButton! @IBOutlet weak var moreButton: UIButton!
@IBOutlet weak var pinImageView: UIImageView!
var statusID: String! var statusID: String!
var accountID: String! var accountID: String!
var reblogStatusID: String?
var rebloggerID: String?
var favorited: Bool = false { var favorited = false {
didSet { didSet {
favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor favoriteButton.tintColor = favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
} }
} }
var reblogged: Bool = false { var reblogged = false {
didSet { didSet {
reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor reblogButton.tintColor = reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : tintColor
} }
} }
var showPinned: Bool = false
var collapsible = false { var collapsible = false {
didSet { didSet {
@ -67,36 +54,36 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
var collapsed = false var collapsed = false
var showStatusAutomatically = false var showStatusAutomatically = false
var avatarURL: URL? var avatarURL: URL?
var updateTimestampWorkItem: DispatchWorkItem?
var attachmentDataTasks: [URLSessionDataTask] = [] var attachmentDataTasks: [URLSessionDataTask] = []
var statusUpdater: Cancellable? private var statusUpdater: Cancellable?
var accountUpdater: Cancellable? private var accountUpdater: Cancellable?
var rebloggerAccountUpdater: Cancellable?
deinit { deinit {
statusUpdater?.cancel() statusUpdater?.cancel()
accountUpdater?.cancel() accountUpdater?.cancel()
rebloggerAccountUpdater?.cancel()
} }
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib()
displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true
attachmentsView.delegate = self attachmentsView.delegate = self
attachmentsView.layer.cornerRadius = 5 attachmentsView.layer.cornerRadius = 5
attachmentsView.layer.masksToBounds = true attachmentsView.layer.masksToBounds = true
collapseButton.layer.masksToBounds = true collapseButton.layer.masksToBounds = true
collapseButton.layer.cornerRadius = 5 collapseButton.layer.cornerRadius = 5
accessibilityElements = [reblogLabel!, displayNameLabel!, contentWarningLabel!, collapseButton!, contentLabel!, attachmentsView!] accessibilityElements = [displayNameLabel!, contentWarningLabel!, collapseButton!, contentLabel!, attachmentsView!]
attachmentsView.isAccessibilityElement = true attachmentsView.isAccessibilityElement = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
@ -110,70 +97,45 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
.filter { $0.id == self.accountID } .filter { $0.id == self.accountID }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: updateUI(account:)) .sink(receiveValue: updateUI(account:))
rebloggerAccountUpdater = MastodonCache.accountSubject
.filter { $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateRebloggerLabel(reblogger:))
} }
func updateUI(statusID: String) { func updateUI(statusID: String) {
guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } guard let status = MastodonCache.status(for: statusID) else {
fatalError("Missing cached status")
}
self.statusID = statusID self.statusID = statusID
if let rebloggedStatusID = status.reblog?.id,
let rebloggedStatus = MastodonCache.status(for: rebloggedStatusID) {
reblogStatusID = statusID
rebloggerID = status.account.id
status = rebloggedStatus
self.statusID = rebloggedStatus.id
reblogLabel.isHidden = false
} else {
reblogStatusID = nil
rebloggerID = nil
reblogLabel.isHidden = true
}
let account = status.account let account = status.account
self.accountID = account.id self.accountID = account.id
updateUI(account: account) updateUI(account: account)
updateUIForPreferences() updateUIForPreferences()
updateTimestamp()
attachmentsView.updateUI(status: status) attachmentsView.updateUI(status: status)
attachmentsView.isAccessibilityElement = status.attachments.count > 0 attachmentsView.isAccessibilityElement = status.attachments.count > 0
attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count) attachmentsView.accessibilityLabel = String(format: NSLocalizedString("%d attachments", comment: "status attachments count accessibility label"), status.attachments.count)
let realStatus = status.reblog ?? status updateStatusState(status: status)
updateStatusState(status: realStatus)
contentLabel.statusID = status.id contentLabel.statusID = statusID
collapsible = !status.spoilerText.isEmpty collapsible = !status.spoilerText.isEmpty
var shouldCollapse = collapsible var shouldCollapse = collapsible
contentWarningLabel.text = status.spoilerText contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty contentWarningLabel.isHidden = status.spoilerText.isEmpty
if !shouldCollapse, if !shouldCollapse,
let text = contentLabel.text, let text = contentLabel.text,
text.count > 500 { text.count > 500 {
collapsible = true collapsible = true
shouldCollapse = true shouldCollapse = true
} }
if collapsible && showStatusAutomatically { if collapsible && showStatusAutomatically {
shouldCollapse = false shouldCollapse = false
} }
setCollapsed(shouldCollapse, animated: false) setCollapsed(shouldCollapse, animated: false)
let pinned = status.pinned ?? false
pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden
} }
private func updateStatusState(status: Status) { func updateStatusState(status: Status) {
favorited = status.favourited ?? false favorited = status.favourited ?? false
reblogged = status.reblogged ?? false reblogged = status.reblogged ?? false
@ -189,12 +151,12 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
private func updateUI(account: Account) { func updateUI(account: Account) {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
avatarImageView.image = nil avatarImageView.image = nil
avatarURL = account.avatar avatarURL = account.avatar
ImageCache.avatars.get(account.avatar) { (data) in ImageCache.avatars.get(account.avatar) { (data) in
guard let data = data else { return } guard let data = data, self.avatarURL == account.avatar else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data) self.avatarImageView.image = UIImage(data: data)
self.avatarURL = nil self.avatarURL = nil
@ -203,64 +165,22 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
@objc func updateUIForPreferences() { @objc func updateUIForPreferences() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("") } guard let account = MastodonCache.account(for: accountID) else { return }
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
if let rebloggerID = rebloggerID,
let reblogger = MastodonCache.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger)
}
displayNameLabel.text = account.realDisplayName displayNameLabel.text = account.realDisplayName
} }
func updateRebloggerLabel(reblogger: Account) {
reblogLabel.text = "Reblogged by \(reblogger.realDisplayName)"
}
func updateTimestamp() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = StatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
let delay: DispatchTimeInterval?
switch status.createdAt.timeAgo().1 {
case .second:
delay = .seconds(10)
case .minute:
delay = .seconds(60)
default:
delay = nil
}
if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem {
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else {
updateTimestampWorkItem = nil
}
}
override func prepareForReuse() { override func prepareForReuse() {
if let url = avatarURL { super.prepareForReuse()
ImageCache.avatars.cancel(url)
if let avatarURL = avatarURL {
ImageCache.avatars.cancel(avatarURL)
} }
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() } attachmentsView.attachmentViews.allObjects.forEach { $0.removeFromSuperview() }
showStatusAutomatically = false showStatusAutomatically = false
showPinned = false
} }
override func setSelected(_ selected: Bool, animated: Bool) { @IBAction func collapseButtonPressed() {
super.setSelected(selected, animated: animated)
if selected {
delegate?.selected(status: statusID)
}
}
@IBAction func collapseButtonPressed(_ sender: Any) {
setCollapsed(!collapsed, animated: true) setCollapsed(!collapsed, animated: true)
delegate?.statusCollapsedStateChanged() delegate?.statusCollapsedStateChanged()
} }
@ -271,7 +191,7 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
contentLabel.isHidden = collapsed contentLabel.isHidden = collapsed
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up") let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
if animated, let buttonImageView = collapseButton.imageView { if animated, let buttonImageView = collapseButton.imageView {
// we need to use a keyframe animation for this, because we want to control the direction the chevron rotates // we need to use a keyframe animation for this, because we want to control the direction the chevron rotates
@ -297,22 +217,14 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} else { } else {
collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label") collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label")
} }
} }
@IBAction func replyPressed(_ sender: Any) { @IBAction func replyPressed() {
delegate?.reply(to: statusID) delegate?.reply(to: statusID)
} }
@objc func accountPressed() { @IBAction func favoritePressed() {
delegate?.selected(account: accountID)
}
@objc func reblogLabelPressed() {
guard let rebloggerID = rebloggerID else { return }
delegate?.selected(account: rebloggerID)
}
@IBAction func favoritePressed(_ sender: Any) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = favorited let oldValue = favorited
@ -337,7 +249,7 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
@IBAction func reblogPressed(_ sender: Any) { @IBAction func reblogPressed() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let oldValue = reblogged let oldValue = reblogged
@ -361,94 +273,20 @@ class StatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
@IBAction func morePressed(_ sender: Any) { @IBAction func morePressed() {
delegate?.showMoreOptions(forStatus: statusID) delegate?.showMoreOptions(forStatus: statusID)
} }
@objc func accountPressed() {
delegate?.selected(account: accountID)
} }
extension StatusTableViewCell: TableViewSwipeActionProvider { func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
return nil
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let favoriteTitle: String
let favoriteRequest: Request<Status>
let favoriteColor: UIColor
if status.favourited ?? false {
favoriteTitle = "Unfavorite"
favoriteRequest = Status.unfavourite(status)
favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else {
favoriteTitle = "Favorite"
favoriteRequest = Status.favourite(status)
favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1)
} }
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
MastodonController.client.run(favoriteRequest, completion: { response in
DispatchQueue.main.async {
guard case let .success(status, _) = response else {
completion(false)
return
}
completion(true)
MastodonCache.add(status: status)
}
})
}
favorite.image = UIImage(systemName: "star.fill")
favorite.backgroundColor = favoriteColor
let reblogTitle: String
let reblogRequest: Request<Status>
let reblogColor: UIColor
if status.reblogged ?? false {
reblogTitle = "Unreblog"
reblogRequest = Status.unreblog(status)
reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else {
reblogTitle = "Reblog"
reblogRequest = Status.reblog(status)
reblogColor = tintColor
}
let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in
MastodonController.client.run(reblogRequest, completion: { response in
DispatchQueue.main.async {
guard case let .success(status, _) = response else {
completion(false)
return
}
completion(true)
MastodonCache.add(status: status)
}
})
}
reblog.image = UIImage(systemName: "repeat")
reblog.backgroundColor = reblogColor
return UISwipeActionsConfiguration(actions: [favorite, reblog])
} }
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? { extension BaseStatusTableViewCell: AttachmentViewDelegate {
let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in
completion(true)
self.delegate?.reply(to: self.statusID)
}
reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill")
reply.backgroundColor = tintColor
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
completion(true)
self.delegate?.showMoreOptions(forStatus: self.statusID)
}
more.image = UIImage(systemName: "ellipsis")
more.backgroundColor = .gray
return UISwipeActionsConfiguration(actions: [reply, more])
}
}
extension StatusTableViewCell: AttachmentViewDelegate {
func showAttachmentsGallery(startingAt index: Int) { func showAttachmentsGallery(startingAt index: Int) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") } guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:)) let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
@ -456,14 +294,13 @@ extension StatusTableViewCell: AttachmentViewDelegate {
} }
} }
extension StatusTableViewCell: MenuPreviewProvider { extension BaseStatusTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
if avatarImageView.frame.contains(location) { if avatarImageView.frame.contains(location) {
return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) }) return (content: { ProfileTableViewController(accountID: self.accountID)}, actions: { self.actionsForProfile(accountID: self.accountID) })
} else if attachmentsView.frame.contains(location) { } else if attachmentsView.frame.contains(location) {
let attachmentsViewLocation = attachmentsView.convert(location, from: self) let attachmentsViewLocation = attachmentsView.convert(location, from: self)
if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView, if let attachmentView = attachmentsView.attachmentViews.allObjects.first(where: { $0.frame.contains(attachmentsViewLocation) }),
let image = attachmentView.image { let image = attachmentView.image {
let description = attachmentView.attachment.description let description = attachmentView.attachment.description
return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] }) return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] })
@ -484,7 +321,6 @@ extension StatusTableViewCell: MenuPreviewProvider {
} }
) )
} }
return (content: { ConversationTableViewController(for: self.statusID) }, actions: { self.actionsForStatus(statusID: self.statusID) }) return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController)
} }
} }

View File

@ -10,7 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
class ConversationMainStatusTableViewCell: UITableViewCell, StatusCell { class ConversationMainStatusTableViewCell: BaseStatusTableViewCell {
static let dateFormatter: DateFormatter = { static let dateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
@ -19,285 +19,55 @@ class ConversationMainStatusTableViewCell: UITableViewCell, StatusCell {
return formatter return formatter
}() }()
var delegate: StatusTableViewCellDelegate? {
didSet {
contentLabel.navigationDelegate = delegate
}
}
@IBOutlet weak var profileDetailContainerView: UIView! @IBOutlet weak var profileDetailContainerView: UIView!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: UILabel!
@IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var contentWarningLabel: UILabel!
@IBOutlet weak var collapseButton: UIButton!
@IBOutlet weak var contentLabel: StatusContentLabel!
@IBOutlet weak var favoriteAndReblogCountStackView: UIStackView! @IBOutlet weak var favoriteAndReblogCountStackView: UIStackView!
@IBOutlet weak var totalFavoritesButton: UIButton! @IBOutlet weak var totalFavoritesButton: UIButton!
@IBOutlet weak var totalReblogsButton: UIButton! @IBOutlet weak var totalReblogsButton: UIButton!
@IBOutlet weak var timestampAndClientLabel: UILabel! @IBOutlet weak var timestampAndClientLabel: UILabel!
@IBOutlet weak var attachmentsView: AttachmentsContainerView!
@IBOutlet weak var replyButton: UIButton!
@IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var reblogButton: UIButton!
@IBOutlet weak var moreButton: UIButton!
var profileAccessibilityElement: UIAccessibilityElement! var profileAccessibilityElement: UIAccessibilityElement!
var statusID: String!
var accountID: String!
var favorited: Bool = false {
didSet {
DispatchQueue.main.async {
self.favoriteButton.tintColor = self.favorited ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : self.tintColor
}
}
}
var reblogged: Bool = false {
didSet {
DispatchQueue.main.async {
self.reblogButton.tintColor = self.reblogged ? UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1) : self.tintColor
}
}
}
var collapsible = false {
didSet {
collapseButton.isHidden = !collapsible
}
}
var collapsed = false
var showStatusAutomatically = false
var avatarURL: URL?
var statusUpdater: Cancellable?
var accountUpdater: Cancellable?
deinit {
statusUpdater?.cancel()
accountUpdater?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) super.awakeFromNib()
usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.layer.masksToBounds = true
attachmentsView.delegate = self
attachmentsView.layer.cornerRadius = 5
attachmentsView.layer.masksToBounds = true
collapseButton.layer.masksToBounds = true
collapseButton.layer.cornerRadius = 5
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self) profileAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self) profileAccessibilityElement.accessibilityFrameInContainerSpace = profileDetailContainerView.convert(profileDetailContainerView.frame, to: self)
accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!] accessibilityElements = [profileAccessibilityElement!, contentWarningLabel!, collapseButton!, contentLabel!, totalFavoritesButton!, totalReblogsButton!, timestampAndClientLabel!, replyButton!, favoriteButton!, reblogButton!, moreButton!]
statusUpdater = MastodonCache.statusSubject
.filter { $0.id == self.statusID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateStatusState(status:))
accountUpdater = MastodonCache.accountSubject
.filter { $0.id == self.accountID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateUI(account:))
} }
func updateUI(statusID: String) { override func updateUI(statusID: String) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } super.updateUI(statusID: statusID)
guard let status = MastodonCache.status(for: statusID) else { fatalError() }
self.statusID = status.id
let account: Account
if let reblog = status.reblog {
account = reblog.account
} else {
account = status.account
}
self.accountID = account.id
updateUI(account: account)
updateUIForPreferences()
var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt) var timestampAndClientText = ConversationMainStatusTableViewCell.dateFormatter.string(from: status.createdAt)
if let application = status.application { if let application = status.application {
timestampAndClientText += "\(application.name)" timestampAndClientText += "\(application.name)"
} }
timestampAndClientLabel.text = timestampAndClientText timestampAndClientLabel.text = timestampAndClientText
attachmentsView.updateUI(status: status)
let realStatus = status.reblog ?? status
updateStatusState(status: realStatus)
contentLabel.statusID = statusID
collapsible = !status.spoilerText.isEmpty
var shouldCollapse = collapsible
contentWarningLabel.text = status.spoilerText
contentWarningLabel.isHidden = status.spoilerText.isEmpty
if collapsible && showStatusAutomatically {
shouldCollapse = false
} }
setCollapsed(shouldCollapse, animated: false) override func updateStatusState(status: Status) {
} super.updateStatusState(status: status)
private func updateStatusState(status: Status) {
favorited = status.favourited ?? false
reblogged = status.reblogged ?? false
if favorited {
favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
} else {
favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
}
if reblogged {
reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
} else {
reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
}
// todo: localize me // todo: localize me
totalFavoritesButton.setTitle("\(status.favouritesCount) Favorite\(status.favouritesCount == 1 ? "" : "s")", for: .normal) totalFavoritesButton.setTitle("\(status.favouritesCount) Favorite\(status.favouritesCount == 1 ? "" : "s")", for: .normal)
totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal) totalReblogsButton.setTitle("\(status.reblogsCount) Reblog\(status.reblogsCount == 1 ? "" : "s")", for: .normal)
} }
private func updateUI(account: Account) { override func updateUI(account: Account) {
usernameLabel.text = "@\(account.acct)" super.updateUI(account: account)
avatarImageView.image = nil
avatarURL = account.avatar
ImageCache.avatars.get(account.avatar) { (data) in
guard let data = data else { return }
DispatchQueue.main.async {
self.avatarImageView.image = UIImage(data: data)
self.avatarURL = nil
}
}
profileAccessibilityElement.accessibilityLabel = account.realDisplayName profileAccessibilityElement.accessibilityLabel = account.realDisplayName
} }
@objc func updateUIForPreferences() { @objc override func updateUIForPreferences() {
guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } super.updateUIForPreferences()
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
displayNameLabel.text = account.realDisplayName
favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts favoriteAndReblogCountStackView.isHidden = !Preferences.shared.showFavoriteAndReblogCounts
} }
override func prepareForReuse() { @IBAction func totalFavoritesPressed() {
if let url = avatarURL {
ImageCache.avatars.cancel(url)
}
attachmentsView.subviews.forEach { $0.removeFromSuperview() }
showStatusAutomatically = false
}
@objc func accountPressed() {
delegate?.selected(account: accountID)
}
@IBAction func collapsePressed(_ sender: Any) {
setCollapsed(!collapsed, animated: true)
delegate?.statusCollapsedStateChanged()
}
func setCollapsed(_ collapsed: Bool, animated: Bool) {
self.collapsed = collapsed
contentLabel.isHidden = collapsed
attachmentsView.isHidden = attachmentsView.attachments.count == 0 || collapsed
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")
if animated, let buttonImageView = collapseButton.imageView {
// see comment in StatusTableViewCell.setCollapsed
UIView.animateKeyframes(withDuration: 0.2, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
buttonImageView.transform = CGAffineTransform(rotationAngle: collapsed ? .pi / 2 : -.pi / 2)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
buttonImageView.transform = CGAffineTransform(rotationAngle: .pi)
}
}, completion: { (finished) in
buttonImageView.transform = .identity
self.collapseButton.setImage(buttonImage, for: .normal)
})
} else {
collapseButton.setImage(buttonImage, for: .normal)
}
if collapsed {
collapseButton.accessibilityLabel = NSLocalizedString("Expand Status", comment: "expand status button accessibility label")
} else {
collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label")
}
}
@IBAction func replyPressed(_ sender: Any) {
delegate?.reply(to: statusID)
}
@IBAction func favoritePressed(_ sender: Any) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
favorited = !favorited
let realStatus: Status = status.reblog ?? status
let request = (favorited ? Status.favourite : Status.unfavourite)(realStatus)
MastodonController.client.run(request) { response in
DispatchQueue.main.async {
if case let .success(newStatus, _) = response {
self.favorited = newStatus.favourited ?? false
MastodonCache.add(status: newStatus)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else {
print("Couldn't favorite status \(realStatus.id)")
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
return
}
}
}
}
@IBAction func reblogPressed(_ sender: Any) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
reblogged = !reblogged
let realStatus: Status = status.reblog ?? status
let request = (reblogged ? Status.reblog : Status.unreblog)(realStatus)
MastodonController.client.run(request) { response in
DispatchQueue.main.async {
if case let .success(newStatus, _) = response {
self.reblogged = newStatus.reblogged ?? false
MastodonCache.add(status: newStatus)
UIImpactFeedbackGenerator(style: .light).impactOccurred()
} else {
print("Couldn't reblog status \(realStatus.id)")
// todo: display error message
UINotificationFeedbackGenerator().notificationOccurred(.error)
}
}
}
}
@IBAction func morePressed(_ sender: Any) {
delegate?.showMoreOptions(forStatus: statusID)
}
@IBAction func totalFavoritesPressed(_ sender: Any) {
if let delegate = delegate { if let delegate = delegate {
// accounts aren't known, pass nil so the VC will load them // accounts aren't known, pass nil so the VC will load them
let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, accountIDs: nil) let vc = delegate.statusActionAccountList(action: .favorite, statusID: statusID, accountIDs: nil)
@ -306,7 +76,7 @@ class ConversationMainStatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
@IBAction func totalReblogsPressed(_ sender: Any) { @IBAction func totalReblogsPressed() {
if let delegate = delegate { if let delegate = delegate {
// accounts aren't known, pass nil so the VC will load them // accounts aren't known, pass nil so the VC will load them
let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, accountIDs: nil) let vc = delegate.statusActionAccountList(action: .reblog, statusID: statusID, accountIDs: nil)
@ -315,42 +85,3 @@ class ConversationMainStatusTableViewCell: UITableViewCell, StatusCell {
} }
} }
} }
extension ConversationMainStatusTableViewCell: AttachmentViewDelegate {
func showAttachmentsGallery(startingAt index: Int) {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
delegate?.showGallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
}
}
extension ConversationMainStatusTableViewCell: MenuPreviewProvider {
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
if avatarImageView.frame.contains(location) {
return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) })
} else if attachmentsView.frame.contains(location) {
let attachmentsViewLocation = attachmentsView.convert(location, from: self)
if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView {
let image = attachmentView.image!
let description = attachmentView.attachment.description
return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] })
}
} else if contentLabel.frame.contains(location),
let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) {
return (
content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) },
actions: {
let text = (self.contentLabel.text! as NSString).substring(with: link.range)
if let mention = self.contentLabel.getMention(for: link.url, text: text) {
return self.actionsForProfile(accountID: mention.id)
} else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) {
return self.actionsForHashtag(hashtag)
} else {
return self.actionsForURL(link.url)
}
}
)
}
return nil
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -72,7 +72,7 @@
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/> <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state> </state>
<connections> <connections>
<action selector="collapsePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="w9d-kB-EaQ"/> <action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="2Jy-L1-lN6"/>
</connections> </connections>
</button> </button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="249" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TgY-hs-Klo" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target"> <label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="249" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TgY-hs-Klo" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
@ -107,7 +107,7 @@
<color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</state> </state>
<connections> <connections>
<action selector="totalFavoritesPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="cj1-BB-TuR"/> <action selector="totalFavoritesPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="D3Y-YB-bqP"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dem-vG-cPB">
@ -119,7 +119,7 @@
<color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <color key="titleColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</state> </state>
<connections> <connections>
<action selector="totalReblogsPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="duG-bV-hcI"/> <action selector="totalReblogsPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="WG3-nQ-jgr"/>
</connections> </connections>
</button> </button>
</subviews> </subviews>
@ -148,7 +148,7 @@
<accessibility key="accessibilityConfiguration" label="Reply"/> <accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/> <state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
<connections> <connections>
<action selector="replyPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="hsh-gx-Swo"/> <action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="RxZ-zv-lkN"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="DhN-rJ-jdA">
@ -156,7 +156,7 @@
<accessibility key="accessibilityConfiguration" label="Favorite"/> <accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/> <state key="normal" image="star.fill" catalog="system"/>
<connections> <connections>
<action selector="favoritePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Hkh-Zo-9Qu"/> <action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="NCA-iR-VMt"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GUG-f7-Hdy">
@ -164,7 +164,7 @@
<accessibility key="accessibilityConfiguration" label="Reblog"/> <accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/> <state key="normal" image="repeat" catalog="system"/>
<connections> <connections>
<action selector="reblogPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="SAf-RN-q8N"/> <action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="iIu-Vv-U0I"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ujo-Ap-dmK">
@ -172,7 +172,7 @@
<accessibility key="accessibilityConfiguration" label="More Actions"/> <accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/> <state key="normal" image="ellipsis" catalog="system"/>
<connections> <connections>
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="dWb-67-CoL"/> <action selector="morePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="1vn-0k-gYi"/>
</connections> </connections>
</button> </button>
</subviews> </subviews>

View File

@ -1,16 +0,0 @@
//
// StatusCell.swift
// Tusker
//
// Created by Shadowfacts on 11/18/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
protocol StatusCell {
var collapsible: Bool { get }
var showStatusAutomatically: Bool { get set }
func setCollapsed(_ collapsed: Bool, animated: Bool)
}

View File

@ -0,0 +1,223 @@
//
// TimelineStatusTableViewCell.swift
// Tusker
//
// Created by Shadowfacts on 8/16/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
import Combine
import Pachyderm
class TimelineStatusTableViewCell: BaseStatusTableViewCell {
static let relativeDateFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric
formatter.unitsStyle = .short
return formatter
}()
@IBOutlet weak var reblogLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
@IBOutlet weak var pinImageView: UIImageView!
var reblogStatusID: String?
var rebloggerID: String?
var showPinned: Bool = false
var updateTimestampWorkItem: DispatchWorkItem?
var rebloggerAccountUpdater: Cancellable?
deinit {
rebloggerAccountUpdater?.cancel()
}
override func awakeFromNib() {
super.awakeFromNib()
reblogLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
accessibilityElements!.insert(reblogLabel!, at: 0)
rebloggerAccountUpdater = MastodonCache.accountSubject
.filter { $0.id == self.rebloggerID }
.receive(on: DispatchQueue.main)
.sink(receiveValue: updateRebloggerLabel(reblogger:))
}
override func updateUI(statusID: String) {
guard var status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") }
let realStatusID: String
if let rebloggedStatusID = status.reblog?.id,
let rebloggedStatus = MastodonCache.status(for: rebloggedStatusID) {
reblogStatusID = statusID
rebloggerID = status.account.id
status = rebloggedStatus
realStatusID = rebloggedStatus.id
reblogLabel.isHidden = false
} else {
reblogStatusID = nil
rebloggerID = nil
reblogLabel.isHidden = true
realStatusID = statusID
}
super.updateUI(statusID: realStatusID)
updateTimestamp()
let pinned = status.pinned ?? false
pinImageView.isHidden = !(pinned && showPinned)
timestampLabel.isHidden = !pinImageView.isHidden
}
@objc override func updateUIForPreferences() {
super.updateUIForPreferences()
if let rebloggerID = rebloggerID,
let reblogger = MastodonCache.account(for: rebloggerID) {
updateRebloggerLabel(reblogger: reblogger)
}
}
private func updateRebloggerLabel(reblogger: Account) {
reblogLabel.text = "Reblogged by \(reblogger.realDisplayName)"
}
func updateTimestamp() {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())
let delay: DispatchTimeInterval?
switch status.createdAt.timeAgo().1 {
case .second:
delay = .seconds(10)
case .minute:
delay = .seconds(60)
default:
delay = nil
}
if let delay = delay {
updateTimestampWorkItem = DispatchWorkItem {
self.updateTimestamp()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
} else {
updateTimestampWorkItem = nil
}
}
override func prepareForReuse() {
super.prepareForReuse()
updateTimestampWorkItem?.cancel()
updateTimestampWorkItem = nil
showPinned = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if selected {
delegate?.selected(status: statusID)
}
}
@objc func reblogLabelPressed() {
guard let rebloggerID = rebloggerID else { return }
delegate?.selected(account: rebloggerID)
}
override func getStatusCellPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> BaseStatusTableViewCell.PreviewProviders? {
return (
content: { ConversationTableViewController(for: self.statusID) },
actions: { self.actionsForStatus(statusID: self.statusID) }
)
}
}
extension TimelineStatusTableViewCell: TableViewSwipeActionProvider {
func leadingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
let favoriteTitle: String
let favoriteRequest: Request<Status>
let favoriteColor: UIColor
if status.favourited ?? false {
favoriteTitle = "Unfavorite"
favoriteRequest = Status.unfavourite(status)
favoriteColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else {
favoriteTitle = "Favorite"
favoriteRequest = Status.favourite(status)
favoriteColor = UIColor(displayP3Red: 1, green: 204/255, blue: 0, alpha: 1)
}
let favorite = UIContextualAction(style: .normal, title: favoriteTitle) { (action, view, completion) in
MastodonController.client.run(favoriteRequest, completion: { response in
DispatchQueue.main.async {
guard case let .success(status, _) = response else {
completion(false)
return
}
completion(true)
MastodonCache.add(status: status)
}
})
}
favorite.image = UIImage(systemName: "star.fill")
favorite.backgroundColor = favoriteColor
let reblogTitle: String
let reblogRequest: Request<Status>
let reblogColor: UIColor
if status.reblogged ?? false {
reblogTitle = "Unreblog"
reblogRequest = Status.unreblog(status)
reblogColor = UIColor(displayP3Red: 235/255, green: 77/255, blue: 62/255, alpha: 1)
} else {
reblogTitle = "Reblog"
reblogRequest = Status.reblog(status)
reblogColor = tintColor
}
let reblog = UIContextualAction(style: .normal, title: reblogTitle) { (action, view, completion) in
MastodonController.client.run(reblogRequest, completion: { response in
DispatchQueue.main.async {
guard case let .success(status, _) = response else {
completion(false)
return
}
completion(true)
MastodonCache.add(status: status)
}
})
}
reblog.image = UIImage(systemName: "repeat")
reblog.backgroundColor = reblogColor
return UISwipeActionsConfiguration(actions: [favorite, reblog])
}
func trailingSwipeActionsConfiguration() -> UISwipeActionsConfiguration? {
let reply = UIContextualAction(style: .normal, title: "Reply") { (action, view, completion) in
completion(true)
self.delegate?.reply(to: self.statusID)
}
reply.image = UIImage(systemName: "arrowshape.turn.up.left.fill")
reply.backgroundColor = tintColor
let more = UIContextualAction(style: .normal, title: "More") { (action, view, completion) in
completion(true)
self.delegate?.showMoreOptions(forStatus: self.statusID)
}
more.image = UIImage(systemName: "ellipsis")
more.backgroundColor = .gray
return UISwipeActionsConfiguration(actions: [reply, more])
}
}

View File

@ -9,7 +9,7 @@
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="StatusTableViewCell" customModule="Tusker" customModuleProvider="target"> <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="TimelineStatusTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="240"/> <rect key="frame" x="0.0" y="0.0" width="375" height="240"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
@ -102,7 +102,7 @@
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/> <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state> </state>
<connections> <connections>
<action selector="collapseButtonPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="HNS-rX-gBM"/> <action selector="collapseButtonPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="JaH-xX-UOD"/>
</connections> </connections>
</button> </button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HrJ-t9-KcD" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target"> <label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Content" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HrJ-t9-KcD" customClass="StatusContentLabel" customModule="Tusker" customModuleProvider="target">
@ -139,7 +139,7 @@
<accessibility key="accessibilityConfiguration" label="Reply"/> <accessibility key="accessibilityConfiguration" label="Reply"/>
<state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/> <state key="normal" image="arrowshape.turn.up.left.fill" catalog="system"/>
<connections> <connections>
<action selector="replyPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Ohg-uU-d3Z"/> <action selector="replyPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="ybz-3W-jAa"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="x0t-TR-jJ4">
@ -147,7 +147,7 @@
<accessibility key="accessibilityConfiguration" label="Favorite"/> <accessibility key="accessibilityConfiguration" label="Favorite"/>
<state key="normal" image="star.fill" catalog="system"/> <state key="normal" image="star.fill" catalog="system"/>
<connections> <connections>
<action selector="favoritePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="gKJ-Hu-za3"/> <action selector="favoritePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="8Q8-Rz-k02"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6tW-z8-Qh9">
@ -155,7 +155,7 @@
<accessibility key="accessibilityConfiguration" label="Reblog"/> <accessibility key="accessibilityConfiguration" label="Reblog"/>
<state key="normal" image="repeat" catalog="system"/> <state key="normal" image="repeat" catalog="system"/>
<connections> <connections>
<action selector="reblogPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="JQI-VT-wTt"/> <action selector="reblogPressed" destination="iN0-l3-epB" eventType="touchUpInside" id="Wa2-ZA-TBo"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="982-J4-NGl">
@ -163,7 +163,7 @@
<accessibility key="accessibilityConfiguration" label="More Actions"/> <accessibility key="accessibilityConfiguration" label="More Actions"/>
<state key="normal" image="ellipsis" catalog="system"/> <state key="normal" image="ellipsis" catalog="system"/>
<connections> <connections>
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="dcV-Ez-EIe"/> <action selector="morePressed" destination="iN0-l3-epB" eventType="touchUpInside" id="WT4-fi-usq"/>
</connections> </connections>
</button> </button>
</subviews> </subviews>
@ -186,20 +186,20 @@
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/> <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections> <connections>
<outlet property="attachmentsView" destination="nbq-yr-2mA" id="GkU-Xk-pc0"/> <outlet property="attachmentsView" destination="nbq-yr-2mA" id="SVm-zl-mPb"/>
<outlet property="avatarImageView" destination="QMP-j2-HLn" id="CAl-hK-i3j"/> <outlet property="avatarImageView" destination="QMP-j2-HLn" id="xfS-v8-Gzu"/>
<outlet property="collapseButton" destination="O0E-Vf-XYR" id="fBb-0C-QA2"/> <outlet property="collapseButton" destination="O0E-Vf-XYR" id="nWd-gg-st8"/>
<outlet property="contentLabel" destination="HrJ-t9-KcD" id="tbD-3T-nNP"/> <outlet property="contentLabel" destination="HrJ-t9-KcD" id="s6V-cx-bBt"/>
<outlet property="contentWarningLabel" destination="inI-Og-YiU" id="2jf-6J-JUU"/> <outlet property="contentWarningLabel" destination="inI-Og-YiU" id="C7a-eK-qcx"/>
<outlet property="displayNameLabel" destination="gll-xe-FSr" id="63y-He-xy1"/> <outlet property="displayNameLabel" destination="gll-xe-FSr" id="vVS-WM-Wqx"/>
<outlet property="favoriteButton" destination="x0t-TR-jJ4" id="Ohz-bs-Ebr"/> <outlet property="favoriteButton" destination="x0t-TR-jJ4" id="guV-yz-Lm6"/>
<outlet property="moreButton" destination="982-J4-NGl" id="Xga-I4-CzK"/> <outlet property="moreButton" destination="982-J4-NGl" id="Pux-tL-aWe"/>
<outlet property="pinImageView" destination="LRh-Cc-1br" id="9jn-0V-PdJ"/> <outlet property="pinImageView" destination="LRh-Cc-1br" id="9jn-0V-PdJ"/>
<outlet property="reblogButton" destination="6tW-z8-Qh9" id="i9h-QA-ZPd"/> <outlet property="reblogButton" destination="6tW-z8-Qh9" id="u2t-8D-kOn"/>
<outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/> <outlet property="reblogLabel" destination="lDH-50-AJZ" id="uJf-Pt-cEP"/>
<outlet property="replyButton" destination="rKF-yF-KIa" id="rul-lk-bIR"/> <outlet property="replyButton" destination="rKF-yF-KIa" id="rka-q1-o4a"/>
<outlet property="timestampLabel" destination="35d-EA-ReR" id="8EW-mb-LAb"/> <outlet property="timestampLabel" destination="35d-EA-ReR" id="Ny2-nV-nqP"/>
<outlet property="usernameLabel" destination="j89-zc-SFa" id="see-Xd-3e9"/> <outlet property="usernameLabel" destination="j89-zc-SFa" id="bXX-FZ-fCp"/>
</connections> </connections>
<point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/> <point key="canvasLocation" x="29.600000000000001" y="79.160419790104953"/>
</view> </view>