Compare commits
No commits in common. "2825345c7e16ef04e65a7beb8802b748510d2555" and "7f0fd119c5df56ce6e678a12c54a45cc0321af63" have entirely different histories.
2825345c7e
...
7f0fd119c5
|
@ -13,7 +13,7 @@ public class Attachment: Codable {
|
||||||
public let kind: Kind
|
public let kind: Kind
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let remoteURL: URL?
|
public let remoteURL: URL?
|
||||||
public let previewURL: URL?
|
public let previewURL: URL
|
||||||
public let textURL: URL?
|
public let textURL: URL?
|
||||||
public let meta: Metadata?
|
public let meta: Metadata?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
|
@ -30,11 +30,11 @@ public class Attachment: Codable {
|
||||||
self.id = try container.decode(String.self, forKey: .id)
|
self.id = try container.decode(String.self, forKey: .id)
|
||||||
self.kind = try container.decode(Kind.self, forKey: .kind)
|
self.kind = try container.decode(Kind.self, forKey: .kind)
|
||||||
self.url = try container.decode(URL.self, forKey: .url)
|
self.url = try container.decode(URL.self, forKey: .url)
|
||||||
self.previewURL = try? container.decode(URL?.self, forKey: .previewURL)
|
self.previewURL = try container.decode(URL.self, forKey: .previewURL)
|
||||||
self.remoteURL = try? container.decode(URL?.self, forKey: .remoteURL)
|
self.remoteURL = try? container.decode(URL.self, forKey: .remoteURL)
|
||||||
self.textURL = try? container.decode(URL?.self, forKey: .textURL)
|
self.textURL = try? container.decode(URL.self, forKey: .textURL)
|
||||||
self.meta = try? container.decode(Metadata?.self, forKey: .meta)
|
self.meta = try? container.decode(Metadata.self, forKey: .meta)
|
||||||
self.description = try? container.decode(String?.self, forKey: .description)
|
self.description = try? container.decode(String.self, forKey: .description)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; };
|
D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; };
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; };
|
||||||
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; };
|
||||||
|
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */; };
|
||||||
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; };
|
D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; };
|
||||||
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
D61AC1D5232E9FA600C54D2D /* InstanceSelectorTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */; };
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */; };
|
||||||
|
@ -118,12 +119,6 @@
|
||||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; };
|
|
||||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0424B0227D00F5412E /* ProfileViewController.swift */; };
|
|
||||||
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */; };
|
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0824B0291E00F5412E /* MyProfileViewController.swift */; };
|
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */; };
|
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
|
||||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6434EB2215B1856001A919A /* XCBRequest.swift */; };
|
||||||
|
@ -155,6 +150,9 @@
|
||||||
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 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
|
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E02134937B0057A976 /* TimelineStatusTableViewCell.xib */; };
|
||||||
|
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */; };
|
||||||
|
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */; };
|
||||||
|
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */; };
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
|
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
|
||||||
D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.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 */; };
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||||
|
@ -381,6 +379,7 @@
|
||||||
D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = "<group>"; };
|
||||||
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
|
D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
D61AC1D4232E9FA600C54D2D /* InstanceSelectorTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelectorTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
D61AC1D6232EA42D00C54D2D /* InstanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -429,12 +428,6 @@
|
||||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatusesViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ProfileHeaderView.xib; sourceTree = "<group>"; };
|
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
|
||||||
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
D6434EB2215B1856001A919A /* XCBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequest.swift; sourceTree = "<group>"; };
|
||||||
|
@ -470,6 +463,9 @@
|
||||||
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 /* TimelineStatusTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TimelineStatusTableViewCell.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>"; };
|
||||||
|
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>"; };
|
||||||
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; 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>"; };
|
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>"; };
|
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -932,9 +928,8 @@
|
||||||
D641C784213DD819004B4513 /* Profile */ = {
|
D641C784213DD819004B4513 /* Profile */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6412B0424B0227D00F5412E /* ProfileViewController.swift */,
|
D667E5E621349D4C0057A976 /* ProfileTableViewController.swift */,
|
||||||
D6412B0624B0237700F5412E /* ProfileStatusesViewController.swift */,
|
D6163F2B21AA0AF1008DAC41 /* MyProfileTableViewController.swift */,
|
||||||
D6412B0824B0291E00F5412E /* MyProfileViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1011,8 +1006,8 @@
|
||||||
D641C78B213DD92F004B4513 /* Profile Header */ = {
|
D641C78B213DD92F004B4513 /* Profile Header */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6412B0A24B0D4C600F5412E /* ProfileHeaderView.xib */,
|
D667E5E821349EE50057A976 /* ProfileHeaderTableViewCell.xib */,
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */,
|
D667E5EA21349EF80057A976 /* ProfileHeaderTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = "Profile Header";
|
path = "Profile Header";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1259,7 +1254,6 @@
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
D6DFC69D242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1516,6 +1510,9 @@
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
||||||
};
|
};
|
||||||
|
D68E526124A3F9AF0054355A = {
|
||||||
|
CreatedOnToolsVersion = 12.0;
|
||||||
|
};
|
||||||
D6D4DDCB212518A000E1C4BB = {
|
D6D4DDCB212518A000E1C4BB = {
|
||||||
CreatedOnToolsVersion = 10.0;
|
CreatedOnToolsVersion = 10.0;
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
|
@ -1590,7 +1587,7 @@
|
||||||
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
D6A3BC8B2321F79B00FD64D5 /* AccountTableViewCell.xib in Resources */,
|
||||||
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
D627FF7D217E958900CC0648 /* DraftTableViewCell.xib in Resources */,
|
||||||
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
D6A3BC7D232195C600FD64D5 /* ActionNotificationGroupTableViewCell.xib in Resources */,
|
||||||
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
|
D667E5E921349EE50057A976 /* ProfileHeaderTableViewCell.xib in Resources */,
|
||||||
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */,
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
|
@ -1724,7 +1721,6 @@
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
D626493C23C1000300612E6E /* AlbumTableViewCell.swift in Sources */,
|
||||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */,
|
||||||
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */,
|
|
||||||
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */,
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
|
@ -1744,7 +1740,6 @@
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
||||||
D6412B0724B0237700F5412E /* ProfileStatusesViewController.swift in Sources */,
|
|
||||||
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
|
@ -1824,7 +1819,6 @@
|
||||||
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
D627943B23A55BA600D38C68 /* NavigableTableViewCell.swift in Sources */,
|
||||||
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */,
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||||
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
D627943523A5525100D38C68 /* StatusActivity.swift in Sources */,
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */,
|
||||||
|
@ -1857,10 +1851,10 @@
|
||||||
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
|
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
|
||||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||||
|
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
|
||||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
||||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
|
||||||
D6412B0524B0227D00F5412E /* ProfileViewController.swift in Sources */,
|
|
||||||
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* CachedDictionary.swift in Sources */,
|
||||||
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */,
|
||||||
|
@ -1876,11 +1870,12 @@
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
||||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */,
|
||||||
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */,
|
||||||
|
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||||
|
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,13 +67,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
presentCompose()
|
presentCompose()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if viewController == viewControllers![selectedIndex],
|
|
||||||
let nav = viewController as? UINavigationController,
|
|
||||||
nav.viewControllers.count == 1,
|
|
||||||
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
|
|
||||||
scrollableVC.tabBarScrollToTop()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +98,7 @@ extension MainTabBarViewController {
|
||||||
case .explore:
|
case .explore:
|
||||||
return ExploreViewController(mastodonController: mastodonController)
|
return ExploreViewController(mastodonController: mastodonController)
|
||||||
case .myProfile:
|
case .myProfile:
|
||||||
return MyProfileViewController(mastodonController: mastodonController)
|
return MyProfileTableViewController(mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// MyProfileViewController.swift
|
// MyProfileTableViewController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 11/24/18.
|
// Created by Shadowfacts on 11/24/18.
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class MyProfileViewController: ProfileViewController {
|
class MyProfileTableViewController: ProfileTableViewController {
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
super.init(accountID: nil, mastodonController: mastodonController)
|
super.init(accountID: nil, mastodonController: mastodonController)
|
||||||
|
@ -17,9 +17,7 @@ class MyProfileViewController: ProfileViewController {
|
||||||
tabBarItem.image = UIImage(systemName: "person.fill")
|
tabBarItem.image = UIImage(systemName: "person.fill")
|
||||||
|
|
||||||
mastodonController.getOwnAccount { (account) in
|
mastodonController.getOwnAccount { (account) in
|
||||||
DispatchQueue.main.async {
|
self.accountID = account.id
|
||||||
self.accountID = account.id
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in
|
_ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in
|
||||||
guard let self = self, let data = data, let image = UIImage(data: data) else { return }
|
guard let self = self, let data = data, let image = UIImage(data: data) else { return }
|
||||||
|
@ -35,7 +33,7 @@ class MyProfileViewController: ProfileViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,4 +47,8 @@ class MyProfileViewController: ProfileViewController {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func closePreferences() {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,319 +0,0 @@
|
||||||
//
|
|
||||||
// ProfileStatusesViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/3/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class ProfileStatusesViewController: EnhancedTableViewController {
|
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
|
||||||
|
|
||||||
private(set) var headerView: ProfileHeaderView!
|
|
||||||
|
|
||||||
var accountID: String!
|
|
||||||
|
|
||||||
let kind: Kind
|
|
||||||
|
|
||||||
private var pinnedStatuses: [(id: String, state: StatusState)] = []
|
|
||||||
private var timelineSegments: [[(id: String, state: StatusState)]] = []
|
|
||||||
|
|
||||||
private var older: RequestRange?
|
|
||||||
private var newer: RequestRange?
|
|
||||||
|
|
||||||
private var loaded = false
|
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, mastodonController: MastodonController) {
|
|
||||||
self.accountID = accountID
|
|
||||||
self.kind = kind
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
super.init(style: .plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
|
||||||
|
|
||||||
refreshControl = UIRefreshControl()
|
|
||||||
refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged)
|
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
|
||||||
tableView.estimatedRowHeight = 140
|
|
||||||
|
|
||||||
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
|
|
||||||
|
|
||||||
tableView.prefetchDataSource = self
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
if !loaded,
|
|
||||||
let accountID = accountID,
|
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID) {
|
|
||||||
loaded = true
|
|
||||||
updateUI(account: account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateUI(account: AccountMO) {
|
|
||||||
if kind == .statuses {
|
|
||||||
getPinnedStatuses { (response) in
|
|
||||||
guard case let .success(statuses, _) = response else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if statuses.isEmpty { return }
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
|
||||||
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 0) }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatuses { (response) in
|
|
||||||
guard case let .success(statuses, pagination) = response else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if statuses.isEmpty { return }
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
|
||||||
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
|
||||||
|
|
||||||
self.older = pagination?.older
|
|
||||||
self.newer = pagination?.newer
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertSections(IndexSet(integer: 1), with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getStatuses(for range: RequestRange = .default, completion: @escaping Client.Callback<[Status]>) {
|
|
||||||
let request: Request<[Status]>
|
|
||||||
switch kind {
|
|
||||||
case .statuses:
|
|
||||||
request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: true)
|
|
||||||
case .withReplies:
|
|
||||||
request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: false, excludeReplies: false)
|
|
||||||
case .onlyMedia:
|
|
||||||
request = Account.getStatuses(accountID, range: range, onlyMedia: true, pinned: false, excludeReplies: false)
|
|
||||||
}
|
|
||||||
mastodonController.run(request, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getPinnedStatuses(completion: @escaping Client.Callback<[Status]>) {
|
|
||||||
let request = Account.getStatuses(accountID, range: .default, onlyMedia: false, pinned: true, excludeReplies: false)
|
|
||||||
mastodonController.run(request, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Interaction
|
|
||||||
|
|
||||||
@objc func refreshStatuses(_ sender: UIRefreshControl) {
|
|
||||||
guard let newer = newer else { return }
|
|
||||||
|
|
||||||
getStatuses(for: newer) { (response) in
|
|
||||||
guard case let .success(newStatuses, pagination) = response else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
|
||||||
// if there's no newer request range (because no statuses were returned),
|
|
||||||
// we don't want to change the current newer pagination, so that we can
|
|
||||||
// continue to load statuses newer than whatever was last loaded
|
|
||||||
if let newer = pagination?.newer {
|
|
||||||
self.newer = newer
|
|
||||||
}
|
|
||||||
|
|
||||||
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: $0, section: 1) }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refreshControl!.endRefreshing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if kind == .statuses {
|
|
||||||
getPinnedStatuses { (response) in
|
|
||||||
guard case let .success(newPinnedStatuses, _) = response else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
|
||||||
let oldPinnedStatuses = self.pinnedStatuses
|
|
||||||
let pinnedStatuses = newPinnedStatuses.map { (status) -> (id: String, state: StatusState) in
|
|
||||||
let state: StatusState
|
|
||||||
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
|
||||||
state = oldState
|
|
||||||
} else {
|
|
||||||
state = .unknown
|
|
||||||
}
|
|
||||||
return (status.id, state)
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.pinnedStatuses = pinnedStatuses
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.reloadSections(IndexSet(integer: 0), with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Table view data source
|
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
// 1 for pinned, rest for timeline
|
|
||||||
return 1 + timelineSegments.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
if section == 0 {
|
|
||||||
return pinnedStatuses.count
|
|
||||||
} else {
|
|
||||||
return timelineSegments[section - 1].count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as! TimelineStatusTableViewCell
|
|
||||||
|
|
||||||
cell.delegate = self
|
|
||||||
|
|
||||||
if indexPath.section == 0 {
|
|
||||||
cell.showPinned = true
|
|
||||||
let (id, state) = pinnedStatuses[indexPath.row]
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
} else {
|
|
||||||
cell.showPinned = false
|
|
||||||
let (id, state) = timelineSegments[indexPath.section - 1][indexPath.row]
|
|
||||||
cell.updateUI(statusID: id, state: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
|
||||||
// todo: if scrolling up, remove statuses at bottom like timeline VC
|
|
||||||
|
|
||||||
// load older statuses if at bottom
|
|
||||||
if timelineSegments.count > 0,
|
|
||||||
indexPath.section == timelineSegments.count,
|
|
||||||
indexPath.row == timelineSegments[indexPath.section - 1].count - 1 {
|
|
||||||
guard let older = older else { return }
|
|
||||||
|
|
||||||
getStatuses(for: older) { (response) in
|
|
||||||
guard case let .success(newStatuses, pagination) = response else {
|
|
||||||
// todo: error message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
|
||||||
// if there is no older request range, we want to set ours to nil
|
|
||||||
// otherwise we would end up loading the same statuses again
|
|
||||||
self.older = pagination?.older
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let start = self.timelineSegments[indexPath.section - 1].count
|
|
||||||
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
|
||||||
self.timelineSegments[indexPath.section - 1].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
self.tableView.insertRows(at: indexPaths, with: .none)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
|
||||||
enum Kind {
|
|
||||||
case statuses, withReplies, onlyMedia
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: StatusTableViewCellDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
|
|
||||||
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
|
||||||
// causes the table view to recalculate the cell heights
|
|
||||||
tableView.beginUpdates()
|
|
||||||
tableView.endUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileStatusesViewController: UITableViewDataSourcePrefetching {
|
|
||||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
|
||||||
for indexPath in indexPaths {
|
|
||||||
let statusID: String
|
|
||||||
if indexPath.section == 0 {
|
|
||||||
statusID = pinnedStatuses[indexPath.row].id
|
|
||||||
} else {
|
|
||||||
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
|
||||||
}
|
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
|
||||||
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
|
||||||
for attachment in status.attachments {
|
|
||||||
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
|
||||||
for indexPath in indexPaths {
|
|
||||||
let statusID: String
|
|
||||||
if indexPath.section == 0 {
|
|
||||||
statusID = pinnedStatuses[indexPath.row].id
|
|
||||||
} else {
|
|
||||||
statusID = timelineSegments[indexPath.section - 1][indexPath.row].id
|
|
||||||
}
|
|
||||||
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
|
||||||
for attachment in status.attachments {
|
|
||||||
ImageCache.avatars.cancelWithoutCallback(attachment.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,355 @@
|
||||||
|
//
|
||||||
|
// ProfileTableViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 8/27/18.
|
||||||
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Pachyderm
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
class ProfileTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
|
var accountID: String!
|
||||||
|
|
||||||
|
var pinnedStatuses: [(id: String, state: StatusState)] = []
|
||||||
|
var timelineSegments: [[(id: String, state: StatusState)]] = []
|
||||||
|
|
||||||
|
var older: RequestRange?
|
||||||
|
var newer: RequestRange?
|
||||||
|
|
||||||
|
private var loadingVC: LoadingViewController? = nil
|
||||||
|
private var loaded = false
|
||||||
|
|
||||||
|
init(accountID: String?, mastodonController: MastodonController) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
self.accountID = accountID
|
||||||
|
|
||||||
|
super.init(style: .plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemeneted")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let id = accountID, let container = mastodonController?.persistentContainer {
|
||||||
|
container.backgroundContext.perform {
|
||||||
|
container.account(for: id, in: container.backgroundContext)?.decrementReferenceCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.refreshControl = UIRefreshControl()
|
||||||
|
refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged)
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:)))
|
||||||
|
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
||||||
|
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: nil), forCellReuseIdentifier: "statusCell")
|
||||||
|
tableView.register(UINib(nibName: "ProfileHeaderTableViewCell", bundle: nil), forCellReuseIdentifier: "headerCell")
|
||||||
|
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
|
|
||||||
|
if accountID == nil {
|
||||||
|
loadingVC = LoadingViewController()
|
||||||
|
embedChild(loadingVC!)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if !loaded, let accountID = accountID {
|
||||||
|
loaded = true
|
||||||
|
loadingVC?.removeViewAndController()
|
||||||
|
loadingVC = nil
|
||||||
|
|
||||||
|
if mastodonController.persistentContainer.account(for: accountID) != nil {
|
||||||
|
updateAccountUI()
|
||||||
|
} else {
|
||||||
|
loadingVC = LoadingViewController()
|
||||||
|
embedChild(loadingVC!)
|
||||||
|
let request = Client.getAccount(id: accountID)
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
guard case let .success(account, _) = response else {
|
||||||
|
let alert = UIAlertController(title: "Something Went Wrong", message: "Couldn't load the selected account", preferredStyle: .alert)
|
||||||
|
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
|
||||||
|
self.navigationController!.popViewController(animated: true)
|
||||||
|
}))
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.present(alert, animated: true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.mastodonController.persistentContainer.addOrUpdate(account: account, incrementReferenceCount: true) { (_) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateAccountUI()
|
||||||
|
self.tableView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAccountUI() {
|
||||||
|
updateUIForPreferences()
|
||||||
|
|
||||||
|
getStatuses(onlyPinned: true) { (response) in
|
||||||
|
guard case let .success(statuses, _) = response else { fatalError() }
|
||||||
|
if statuses.isEmpty { return }
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
self.pinnedStatuses = statuses.map { ($0.id, .unknown) }
|
||||||
|
let indexPaths = (0..<statuses.count).map { IndexPath(row: $0, section: 1) }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatuses() { response in
|
||||||
|
guard case let .success(statuses, pagination) = response else { fatalError() }
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: statuses) {
|
||||||
|
self.timelineSegments.append(statuses.map { ($0.id, .unknown) })
|
||||||
|
|
||||||
|
self.older = pagination?.older
|
||||||
|
self.newer = pagination?.newer
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertSections(IndexSet(integer: 2), with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateUIForPreferences() {
|
||||||
|
guard let accountID = accountID, let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
||||||
|
navigationItem.title = account.displayNameWithoutCustomEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatuses(for range: RequestRange = .default, onlyPinned: Bool = false, completion: @escaping Client.Callback<[Status]>) {
|
||||||
|
let request = Account.getStatuses(accountID, range: range, onlyMedia: false, pinned: onlyPinned, excludeReplies: !Preferences.shared.showRepliesInProfiles)
|
||||||
|
mastodonController.run(request, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessageMentioning() {
|
||||||
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
|
let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, mastodonController: mastodonController))
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Table view data source
|
||||||
|
|
||||||
|
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
// 1 section for header, 1 section for pinned, rest for timeline
|
||||||
|
return 2 + timelineSegments.count
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
if section == 0 {
|
||||||
|
return accountID == nil || mastodonController.persistentContainer.account(for: accountID) == nil ? 0 : 1
|
||||||
|
} else if section == 1 {
|
||||||
|
return pinnedStatuses.count
|
||||||
|
} else {
|
||||||
|
return timelineSegments[section - 2].count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
switch indexPath.section {
|
||||||
|
case 0:
|
||||||
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "headerCell", for: indexPath) as? ProfileHeaderTableViewCell else { fatalError() }
|
||||||
|
cell.selectionStyle = .none
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(for: accountID)
|
||||||
|
return cell
|
||||||
|
case 1:
|
||||||
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||||
|
let (id, state) = pinnedStatuses[indexPath.row]
|
||||||
|
cell.showPinned = true
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
return cell
|
||||||
|
default:
|
||||||
|
guard let cell = tableView.dequeueReusableCell(withIdentifier: "statusCell", for: indexPath) as? TimelineStatusTableViewCell else { fatalError() }
|
||||||
|
let (id, state) = timelineSegments[indexPath.section - 2][indexPath.row]
|
||||||
|
cell.delegate = self
|
||||||
|
cell.updateUI(statusID: id, state: state)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Table view delegate
|
||||||
|
|
||||||
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
|
// todo: if scrolling up, remove statuses at bottom like timeline VC
|
||||||
|
|
||||||
|
// load older statuses if at bottom
|
||||||
|
if timelineSegments.count > 0 && indexPath.section - 1 == timelineSegments.count && indexPath.row == timelineSegments[indexPath.section - 2].count - 1 {
|
||||||
|
guard let older = older else { return }
|
||||||
|
|
||||||
|
getStatuses(for: older) { response in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
|
self.older = pagination?.older
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let start = self.timelineSegments[indexPath.section - 2].count
|
||||||
|
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: start + $0, section: indexPath.section) }
|
||||||
|
self.timelineSegments[indexPath.section - 2].append(contentsOf: newStatuses.map { ($0.id, .unknown) })
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func refreshStatuses(_ sender: Any) {
|
||||||
|
guard let newer = newer else { return }
|
||||||
|
|
||||||
|
getStatuses(for: newer) { response in
|
||||||
|
guard case let .success(newStatuses, pagination) = response else { fatalError() }
|
||||||
|
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newStatuses) {
|
||||||
|
if let newer = pagination?.newer {
|
||||||
|
self.newer = newer
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexPaths = (0..<newStatuses.count).map { IndexPath(row: $0, section: 2) }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.timelineSegments[0].insert(contentsOf: newStatuses.map { ($0.id, .unknown) }, at: 0)
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.insertRows(at: indexPaths, with: .none)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshControl?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatuses(onlyPinned: true) { (response) in
|
||||||
|
guard case let .success(newPinnedStatuses, _) = response else { fatalError() }
|
||||||
|
self.mastodonController.persistentContainer.addAll(statuses: newPinnedStatuses) {
|
||||||
|
let oldPinnedStatuses = self.pinnedStatuses
|
||||||
|
var pinnedStatuses = [(id: String, state: StatusState)]()
|
||||||
|
for status in newPinnedStatuses {
|
||||||
|
let state: StatusState
|
||||||
|
if let (_, oldState) = oldPinnedStatuses.first(where: { $0.id == status.id }) {
|
||||||
|
state = oldState
|
||||||
|
} else {
|
||||||
|
state = .unknown
|
||||||
|
}
|
||||||
|
pinnedStatuses.append((status.id, state))
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.pinnedStatuses = pinnedStatuses
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.tableView.reloadSections(IndexSet(integer: 1), with: .none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func composePressed(_ sender: Any) {
|
||||||
|
sendMessageMentioning()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileTableViewController: StatusTableViewCellDelegate {
|
||||||
|
var apiController: MastodonController { mastodonController }
|
||||||
|
|
||||||
|
func statusCellCollapsedStateChanged(_ cell: BaseStatusTableViewCell) {
|
||||||
|
// causes the table view to recalculate the cell heights
|
||||||
|
tableView.beginUpdates()
|
||||||
|
tableView.endUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate {
|
||||||
|
func showMoreOptions(cell: ProfileHeaderTableViewCell) {
|
||||||
|
let account = mastodonController.persistentContainer.account(for: accountID)!
|
||||||
|
|
||||||
|
func showActivityController(activities: [UIActivity]) {
|
||||||
|
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
||||||
|
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
||||||
|
activityController.popoverPresentationController?.sourceView = cell.moreButtonVisualEffectView
|
||||||
|
self.present(activityController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.id == mastodonController.account.id {
|
||||||
|
showActivityController(activities: [OpenInSafariActivity()])
|
||||||
|
} else {
|
||||||
|
let request = Client.getRelationships(accounts: [account.id])
|
||||||
|
mastodonController.run(request) { (response) in
|
||||||
|
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
||||||
|
if case let .success(results, _) = response, let relationship = results.first {
|
||||||
|
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
||||||
|
customActivities.insert(toggleFollowActivity, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
showActivityController(activities: customActivities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ProfileTableViewController: UITableViewDataSourcePrefetching {
|
||||||
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
for indexPath in indexPaths where indexPath.section > 1 {
|
||||||
|
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||||
|
_ = ImageCache.avatars.get(status.account.avatar, completion: nil)
|
||||||
|
for attachment in status.attachments {
|
||||||
|
_ = ImageCache.attachments.get(attachment.url, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {
|
||||||
|
for indexPath in indexPaths where indexPath.section > 1 {
|
||||||
|
let statusID = timelineSegments[indexPath.section - 2][indexPath.row].id
|
||||||
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else { continue }
|
||||||
|
ImageCache.avatars.cancelWithoutCallback(status.account.avatar)
|
||||||
|
for attachment in status.attachments {
|
||||||
|
ImageCache.attachments.cancelWithoutCallback(attachment.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,193 +0,0 @@
|
||||||
//
|
|
||||||
// ProfileViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/3/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
class ProfileViewController: UIPageViewController {
|
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
|
||||||
|
|
||||||
// todo: does this still need to be settable?
|
|
||||||
var accountID: String! {
|
|
||||||
didSet {
|
|
||||||
updateAccountUI()
|
|
||||||
pageControllers.forEach { $0.accountID = accountID }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var accountUpdater: Cancellable?
|
|
||||||
|
|
||||||
private(set) var currentIndex: Int!
|
|
||||||
let pageControllers: [ProfileStatusesViewController]
|
|
||||||
var currentViewController: ProfileStatusesViewController {
|
|
||||||
pageControllers[currentIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
private var headerView: ProfileHeaderView!
|
|
||||||
|
|
||||||
init(accountID: String?, mastodonController: MastodonController) {
|
|
||||||
self.accountID = accountID
|
|
||||||
self.mastodonController = mastodonController
|
|
||||||
|
|
||||||
self.pageControllers = [
|
|
||||||
ProfileStatusesViewController(accountID: accountID, kind: .statuses, mastodonController: mastodonController),
|
|
||||||
ProfileStatusesViewController(accountID: accountID, kind: .withReplies, mastodonController: mastodonController),
|
|
||||||
ProfileStatusesViewController(accountID: accountID, kind: .onlyMedia, mastodonController: mastodonController)
|
|
||||||
]
|
|
||||||
|
|
||||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
|
||||||
|
|
||||||
headerView = ProfileHeaderView.create()
|
|
||||||
headerView.delegate = self
|
|
||||||
headerView.updateUI(for: accountID)
|
|
||||||
|
|
||||||
selectPage(at: 0, animated: false)
|
|
||||||
|
|
||||||
currentViewController.tableView.tableHeaderView = headerView
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
headerView.widthAnchor.constraint(equalTo: view.widthAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] (_) in self?.updateAccountUI() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAccountUI() {
|
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else { return }
|
|
||||||
navigationItem.title = account.displayNameWithoutCustomEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
|
|
||||||
let direction: UIPageViewController.NavigationDirection = currentIndex == nil || index - currentIndex > 0 ? .forward : .reverse
|
|
||||||
currentIndex = index
|
|
||||||
|
|
||||||
guard let old = viewControllers?.first as? ProfileStatusesViewController else {
|
|
||||||
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
|
|
||||||
// since it will be added in viewDidLoad
|
|
||||||
setViewControllers([pageControllers[index]], direction: direction, animated: animated, completion: completion)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let new = pageControllers[index]
|
|
||||||
|
|
||||||
let headerHeight = self.headerView.bounds.height
|
|
||||||
|
|
||||||
// Store old's content offset so it can be transferred to new
|
|
||||||
let prevOldContentOffset = old.tableView.contentOffset
|
|
||||||
// Remove the header, inset the table content by the same amount, and adjust the offset so the cells don't move
|
|
||||||
old.tableView.tableHeaderView = nil
|
|
||||||
old.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
|
||||||
old.tableView.contentOffset.y -= headerHeight
|
|
||||||
|
|
||||||
// Add the header to ourself temporarily, and constrain it to the same position it was in
|
|
||||||
self.view.addSubview(self.headerView)
|
|
||||||
let tempTopConstraint = self.headerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -(prevOldContentOffset.y + old.tableView.safeAreaInsets.top))
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
|
|
||||||
tempTopConstraint
|
|
||||||
])
|
|
||||||
|
|
||||||
// Setup the inset in new, in case it hasn't been already
|
|
||||||
new.tableView.contentInset = UIEdgeInsets(top: headerHeight, left: 0, bottom: 0, right: 0)
|
|
||||||
// Match the scroll positions
|
|
||||||
new.tableView.contentOffset = old.tableView.contentOffset
|
|
||||||
|
|
||||||
// Actually switch pages
|
|
||||||
setViewControllers([pageControllers[index]], direction: direction, animated: animated) { (finished) in
|
|
||||||
// Defer everything one run-loop iteration, otherwise altering the tableView's contentInset/Offset causes it to jump around during the animation
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
// Move the header to the new table view
|
|
||||||
new.tableView.tableHeaderView = self.headerView
|
|
||||||
// Remove the inset, and set the offset back to old's original one, prior to removing the header
|
|
||||||
new.tableView.contentInset = .zero
|
|
||||||
new.tableView.contentOffset = prevOldContentOffset
|
|
||||||
|
|
||||||
// Deactivate the top constraint, otherwise it sticks around
|
|
||||||
tempTopConstraint.isActive = false
|
|
||||||
// Re-add the width constraint since it was removed by re-parenting the view
|
|
||||||
// Why was the width constraint removed, but the top one not? Good question, I have no idea.
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
self.headerView.widthAnchor.constraint(equalTo: self.view.widthAnchor)
|
|
||||||
])
|
|
||||||
|
|
||||||
// Layout and update the table view, otherwise the content jumps around when first scrolling it,
|
|
||||||
// if old was not scrolled all the way to the top
|
|
||||||
new.tableView.layoutIfNeeded()
|
|
||||||
UIView.performWithoutAnimation {
|
|
||||||
new.tableView.performBatchUpdates(nil, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
completion?(finished)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController: TuskerNavigationDelegate {
|
|
||||||
var apiController: MastodonController { mastodonController }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
|
||||||
func profileHeader(_ view: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int) {
|
|
||||||
// disable user interaction on segmented control while switching pages to prevent
|
|
||||||
// race condition from trying to switch to multiple pages simultaneously
|
|
||||||
view.pagesSegmentedControl.isUserInteractionEnabled = false
|
|
||||||
selectPage(at: newIndex, animated: true) { (finished) in
|
|
||||||
view.pagesSegmentedControl.isUserInteractionEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func profileHeader(_ view: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView) {
|
|
||||||
let account = mastodonController.persistentContainer.account(for: accountID)!
|
|
||||||
|
|
||||||
func showActivityController(activities: [UIActivity]) {
|
|
||||||
let activityController = UIActivityViewController(activityItems: [account.url, AccountActivityItemSource(account)], applicationActivities: activities)
|
|
||||||
activityController.completionWithItemsHandler = OpenInSafariActivity.completionHandler(viewController: self, url: account.url)
|
|
||||||
activityController.popoverPresentationController?.sourceView = sourceView
|
|
||||||
self.present(activityController, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account.id == mastodonController.account.id {
|
|
||||||
showActivityController(activities: [OpenInSafariActivity()])
|
|
||||||
} else {
|
|
||||||
let request = Client.getRelationships(accounts: [account.id])
|
|
||||||
mastodonController.run(request) { (response) in
|
|
||||||
var customActivities: [UIActivity] = [OpenInSafariActivity()]
|
|
||||||
if case let .success(results, _) = response, let relationship = results.first {
|
|
||||||
let toggleFollowActivity = relationship.following ? UnfollowAccountActivity() : FollowAccountActivity()
|
|
||||||
customActivities.insert(toggleFollowActivity, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
showActivityController(activities: customActivities)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ProfileViewController: TabBarScrollableViewController {
|
|
||||||
func tabBarScrollToTop() {
|
|
||||||
pageControllers[currentIndex].tabBarScrollToTop()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -95,13 +95,3 @@ extension EnhancedTableViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EnhancedTableViewController: TabBarScrollableViewController {
|
|
||||||
func tabBarScrollToTop() {
|
|
||||||
if scrollViewShouldScrollToTop(tableView) {
|
|
||||||
let topOffset = CGPoint(x: 0, y: -tableView.adjustedContentInset.top)
|
|
||||||
tableView.setContentOffset(topOffset, animated: true)
|
|
||||||
scrollViewDidScrollToTop(tableView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -63,11 +63,3 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SegmentedPageViewController: TabBarScrollableViewController {
|
|
||||||
func tabBarScrollToTop() {
|
|
||||||
if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController {
|
|
||||||
scrollableVC.tabBarScrollToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
//
|
|
||||||
// TabBarScrollableViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 7/3/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol TabBarScrollableViewController: UIViewController {
|
|
||||||
func tabBarScrollToTop()
|
|
||||||
}
|
|
|
@ -67,16 +67,16 @@ extension TuskerNavigationDelegate where Self: UIViewController {
|
||||||
|
|
||||||
func selected(account accountID: String) {
|
func selected(account accountID: String) {
|
||||||
// don't open if the account is the same as the current one
|
// don't open if the account is the same as the current one
|
||||||
if let profileController = self as? ProfileViewController,
|
if let profileController = self as? ProfileTableViewController,
|
||||||
profileController.accountID == accountID {
|
profileController.accountID == accountID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
show(ProfileViewController(accountID: accountID, mastodonController: apiController), sender: self)
|
show(ProfileTableViewController(accountID: accountID, mastodonController: apiController), sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(mention: Mention) {
|
func selected(mention: Mention) {
|
||||||
show(ProfileViewController(accountID: mention.id, mastodonController: apiController), sender: self)
|
show(ProfileTableViewController(accountID: mention.id, mastodonController: apiController), sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selected(tag: Hashtag) {
|
func selected(tag: Hashtag) {
|
||||||
|
|
|
@ -83,7 +83,7 @@ extension AccountTableViewCell: MenuPreviewProvider {
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
return (
|
return (
|
||||||
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||||
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@ class ContentTextView: LinkTextView {
|
||||||
let text = (self.text as NSString).substring(with: range)
|
let text = (self.text as NSString).substring(with: range)
|
||||||
|
|
||||||
if let mention = getMention(for: url, text: text) {
|
if let mention = getMention(for: url, text: text) {
|
||||||
return ProfileViewController(accountID: mention.id, mastodonController: mastodonController!)
|
return ProfileTableViewController(accountID: mention.id, mastodonController: mastodonController!)
|
||||||
} else if let tag = getHashtag(for: url, text: text) {
|
} else if let tag = getHashtag(for: url, text: text) {
|
||||||
return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!)
|
return HashtagTimelineViewController(for: tag, mastodonController: mastodonController!)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -147,7 +147,7 @@ extension FollowNotificationGroupTableViewCell: MenuPreviewProvider {
|
||||||
let accountIDs = self.group.notifications.map { $0.account.id }
|
let accountIDs = self.group.notifications.map { $0.account.id }
|
||||||
return (content: {
|
return (content: {
|
||||||
if accountIDs.count == 1 {
|
if accountIDs.count == 1 {
|
||||||
return ProfileViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
|
return ProfileTableViewController(accountID: accountIDs.first!, mastodonController: mastodonController)
|
||||||
} else {
|
} else {
|
||||||
return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
return AccountListTableViewController(accountIDs: accountIDs, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,7 @@ extension FollowRequestNotificationTableViewCell: MenuPreviewProvider {
|
||||||
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
return (content: {
|
return (content: {
|
||||||
return ProfileViewController(accountID: self.account.id, mastodonController: mastodonController)
|
return ProfileTableViewController(accountID: self.account.id, mastodonController: mastodonController)
|
||||||
}, actions: {
|
}, actions: {
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,35 +1,27 @@
|
||||||
//
|
//
|
||||||
// ProfileHeaderView.swift
|
// ProfileHeaderTableViewCell.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 7/4/20.
|
// Created by Shadowfacts on 8/27/18.
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate {
|
protocol ProfileHeaderTableViewCellDelegate: TuskerNavigationDelegate {
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, selectedPostsIndexChangedTo newIndex: Int)
|
func showMoreOptions(cell: ProfileHeaderTableViewCell)
|
||||||
|
|
||||||
func profileHeader(_ headerView: ProfileHeaderView, showMoreOptionsFor accountID: String, sourceView: UIView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfileHeaderView: UIView {
|
class ProfileHeaderTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
static func create() -> ProfileHeaderView {
|
weak var delegate: ProfileHeaderTableViewCellDelegate?
|
||||||
let nib = UINib(nibName: "ProfileHeaderView", bundle: .main)
|
|
||||||
return nib.instantiate(withOwner: nil, options: nil).first as! ProfileHeaderView
|
|
||||||
}
|
|
||||||
|
|
||||||
weak var delegate: ProfileHeaderViewDelegate?
|
|
||||||
var mastodonController: MastodonController! { delegate?.apiController }
|
var mastodonController: MastodonController! { delegate?.apiController }
|
||||||
|
|
||||||
@IBOutlet weak var headerImageView: UIImageView!
|
@IBOutlet weak var headerImageView: UIImageView!
|
||||||
@IBOutlet weak var avatarContainerView: UIView!
|
@IBOutlet weak var avatarContainerView: UIView!
|
||||||
@IBOutlet weak var avatarImageView: UIImageView!
|
@IBOutlet weak var avatarImageView: UIImageView!
|
||||||
@IBOutlet weak var moreButton: VisualEffectImageButton!
|
|
||||||
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
@IBOutlet weak var displayNameLabel: EmojiLabel!
|
||||||
@IBOutlet weak var usernameLabel: UILabel!
|
@IBOutlet weak var usernameLabel: UILabel!
|
||||||
@IBOutlet weak var followsYouLabel: UILabel!
|
@IBOutlet weak var followsYouLabel: UILabel!
|
||||||
|
@ -37,7 +29,7 @@ class ProfileHeaderView: UIView {
|
||||||
@IBOutlet weak var fieldsStackView: UIStackView!
|
@IBOutlet weak var fieldsStackView: UIStackView!
|
||||||
@IBOutlet weak var fieldNamesStackView: UIStackView!
|
@IBOutlet weak var fieldNamesStackView: UIStackView!
|
||||||
@IBOutlet weak var fieldValuesStackView: UIStackView!
|
@IBOutlet weak var fieldValuesStackView: UIStackView!
|
||||||
@IBOutlet weak var pagesSegmentedControl: UISegmentedControl!
|
@IBOutlet weak var moreButton: VisualEffectImageButton!
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
|
@ -46,39 +38,17 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
private var accountUpdater: Cancellable?
|
private var accountUpdater: Cancellable?
|
||||||
|
|
||||||
override var frame: CGRect {
|
|
||||||
didSet {
|
|
||||||
print("set header frame for \(self)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var bounds: CGRect {
|
|
||||||
didSet {
|
|
||||||
print("set header bounds for \(self)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
avatarRequest?.cancel()
|
|
||||||
headerRequest?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
|
||||||
|
|
||||||
avatarContainerView.layer.masksToBounds = true
|
avatarContainerView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))
|
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed)))
|
||||||
avatarImageView.isUserInteractionEnabled = true
|
avatarImageView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed)))
|
headerImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(headerPressed)))
|
||||||
headerImageView.isUserInteractionEnabled = true
|
headerImageView.isUserInteractionEnabled = true
|
||||||
|
|
||||||
moreButton.layer.cornerRadius = 16
|
moreButton.layer.cornerRadius = 16
|
||||||
moreButton.layer.masksToBounds = true
|
moreButton.layer.masksToBounds = true
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
|
||||||
|
|
||||||
if #available(iOS 13.4, *) {
|
if #available(iOS 13.4, *) {
|
||||||
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
moreButton.addInteraction(UIPointerInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
@ -86,14 +56,15 @@ class ProfileHeaderView: UIView {
|
||||||
moreButton.showsMenuAsPrimaryAction = true
|
moreButton.showsMenuAsPrimaryAction = true
|
||||||
moreButton.isContextMenuInteractionEnabled = true
|
moreButton.isContextMenuInteractionEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(for accountID: String) {
|
func updateUI(for accountID: String) {
|
||||||
|
guard accountID != self.accountID else { return }
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
|
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID)") }
|
||||||
fatalError("Missing cached account \(accountID)")
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUIForPreferences()
|
updateUIForPreferences()
|
||||||
|
|
||||||
|
@ -125,14 +96,11 @@ class ProfileHeaderView: UIView {
|
||||||
// don't show relationship label for the user's own account
|
// don't show relationship label for the user's own account
|
||||||
if accountID != mastodonController.account.id {
|
if accountID != mastodonController.account.id {
|
||||||
let request = Client.getRelationships(accounts: [accountID])
|
let request = Client.getRelationships(accounts: [accountID])
|
||||||
mastodonController.run(request) { [weak self] (response) in
|
mastodonController.run(request) { (response) in
|
||||||
guard let self = self,
|
if case let .success(results, _) = response, let relationship = results.first {
|
||||||
case let .success(results, _) = response,
|
DispatchQueue.main.async {
|
||||||
let relationship = results.first else {
|
self.followsYouLabel.isHidden = !relationship.followedBy
|
||||||
return
|
}
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.followsYouLabel.isHidden = !relationship.followedBy
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,60 +135,51 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
if accountUpdater == nil {
|
if accountUpdater == nil {
|
||||||
accountUpdater = mastodonController.persistentContainer.accountSubject
|
accountUpdater = mastodonController.persistentContainer.accountSubject
|
||||||
.filter { [weak self] in $0 == self?.accountID }
|
.filter { [unowned self] in $0 == self.accountID }
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] in self?.updateUI(for: $0) }
|
.sink { [unowned self] in self.updateUI(for: $0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
fatalError("Missing cached account \(accountID!)")
|
|
||||||
}
|
|
||||||
|
|
||||||
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
override func prepareForReuse() {
|
||||||
|
avatarRequest?.cancel()
|
||||||
|
headerRequest?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func morePressed(_ sender: Any) {
|
@IBAction func morePressed(_ sender: Any) {
|
||||||
guard #available(iOS 14.0, *) else {
|
delegate?.showMoreOptions(cell: self)
|
||||||
// can't use TuskerNavigationDelegate method, because it doesn't know about the (un)follow activity
|
|
||||||
delegate?.profileHeader(self, showMoreOptionsFor: accountID, sourceView: moreButton)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func avatarPressed() {
|
@objc func avatarPressed() {
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
fatalError("Missing cached account \(accountID!)")
|
|
||||||
}
|
|
||||||
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
delegate?.showLoadingLargeImage(url: account.avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func headerPressed() {
|
@objc func headerPressed() {
|
||||||
guard let account = mastodonController.persistentContainer.account(for: accountID) else {
|
guard let account = mastodonController.persistentContainer.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") }
|
||||||
fatalError("Missing cached account \(accountID!)")
|
|
||||||
}
|
|
||||||
delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
delegate?.showLoadingLargeImage(url: account.header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func postsSegmentedControlChanged(_ sender: UISegmentedControl) {
|
|
||||||
delegate?.profileHeader(self, selectedPostsIndexChangedTo: sender.selectedSegmentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13.4, *)
|
@available(iOS 13.4, *)
|
||||||
extension ProfileHeaderView: UIPointerInteractionDelegate {
|
extension ProfileHeaderTableViewCell: UIPointerInteractionDelegate {
|
||||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||||
let preview = UITargetedPreview(view: moreButton)
|
let preview = UITargetedPreview(view: moreButton)
|
||||||
return UIPointerStyle(effect: .lift(preview), shape: .none)
|
return UIPointerStyle(effect: .lift(preview), shape: .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderView: MenuPreviewProvider {
|
extension ProfileHeaderTableViewCell: MenuPreviewProvider {
|
||||||
var navigationDelegate: TuskerNavigationDelegate? { delegate }
|
var navigationDelegate: TuskerNavigationDelegate? {
|
||||||
|
delegate
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17132" 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="17105"/>
|
||||||
|
<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"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ProfileHeaderTableViewCell" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="296"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Fw7-OL-iy5">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" label="User Profile Banner">
|
||||||
|
<bool key="isElement" value="YES"/>
|
||||||
|
</accessibility>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="150" id="y43-4B-slK"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KyB-ey-l11">
|
||||||
|
<rect key="frame" x="16" y="90" width="120" height="120"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="tH8-sR-DHC">
|
||||||
|
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
||||||
|
<accessibility key="accessibilityConfiguration" label="User Avatar">
|
||||||
|
<bool key="isElement" value="YES"/>
|
||||||
|
</accessibility>
|
||||||
|
<gestureRecognizers/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="116" id="k9v-I0-Aoz"/>
|
||||||
|
<constraint firstAttribute="height" constant="116" id="sz5-86-5Iq"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerX" secondItem="KyB-ey-l11" secondAttribute="centerX" id="KT6-FP-LsA"/>
|
||||||
|
<constraint firstAttribute="height" constant="120" id="LVm-OC-cGm"/>
|
||||||
|
<constraint firstAttribute="width" constant="120" id="Obt-ZN-POD"/>
|
||||||
|
<constraint firstItem="tH8-sR-DHC" firstAttribute="centerY" secondItem="KyB-ey-l11" secondAttribute="centerY" id="nYu-RE-MfA"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="LjK-72-Bez" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="144" y="158" width="215" height="24"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MIj-OR-NOR">
|
||||||
|
<rect key="frame" x="144" y="190" width="215" height="18"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="DfO-uD-UNI">
|
||||||
|
<rect key="frame" x="16" y="218" width="343" height="12"/>
|
||||||
|
<subviews>
|
||||||
|
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a32-1a-xXZ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bnc-3t-t7t" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335.5" height="12"/>
|
||||||
|
<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="17"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sHU-GU-klv">
|
||||||
|
<rect key="frame" x="16" y="238" width="343" height="50"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="pV2-Mz-54W">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="167.5" height="50"/>
|
||||||
|
</stackView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="oza-9d-8v4">
|
||||||
|
<rect key="frame" x="175.5" y="0.0" width="167.5" height="50"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="lessThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="2" id="LHm-6k-LyV"/>
|
||||||
|
<constraint firstItem="oza-9d-8v4" firstAttribute="width" relation="greaterThanOrEqual" secondItem="pV2-Mz-54W" secondAttribute="width" multiplier="0.5" id="Zbr-l3-Lff"/>
|
||||||
|
</constraints>
|
||||||
|
</stackView>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7wb-qe-IRt" customClass="VisualEffectImageButton" customModule="Tusker" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="335" y="110" width="32" height="32"/>
|
||||||
|
<accessibility key="accessibilityConfiguration">
|
||||||
|
<accessibilityTraits key="traits" button="YES"/>
|
||||||
|
</accessibility>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="32" id="vwR-Sm-L8e"/>
|
||||||
|
<constraint firstAttribute="width" constant="32" id="w1W-PQ-czc"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="image" keyPath="image" value="ellipsis" catalog="system"/>
|
||||||
|
</userDefinedRuntimeAttributes>
|
||||||
|
<connections>
|
||||||
|
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="7vJ-y3-FdZ"/>
|
||||||
|
</connections>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="0fI-0y-cXG"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="DfO-uD-UNI" secondAttribute="trailing" constant="16" id="ASp-mh-SFv"/>
|
||||||
|
<constraint firstItem="KyB-ey-l11" firstAttribute="centerY" secondItem="Fw7-OL-iy5" secondAttribute="bottom" id="AXr-6X-FJ8"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="MIj-OR-NOR" secondAttribute="trailing" constant="16" id="AwT-Vi-CLa"/>
|
||||||
|
<constraint firstItem="LjK-72-Bez" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="CIO-tn-hJC"/>
|
||||||
|
<constraint firstItem="LjK-72-Bez" firstAttribute="top" secondItem="Fw7-OL-iy5" secondAttribute="bottom" constant="8" id="Kvl-sz-Lv3"/>
|
||||||
|
<constraint firstItem="DfO-uD-UNI" firstAttribute="top" secondItem="KyB-ey-l11" secondAttribute="bottom" constant="8" id="Ljh-8x-yI3"/>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" id="LqH-lE-AIe"/>
|
||||||
|
<constraint firstItem="KyB-ey-l11" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="NN7-5B-k1Q"/>
|
||||||
|
<constraint firstItem="MIj-OR-NOR" firstAttribute="bottom" secondItem="tH8-sR-DHC" secondAttribute="bottom" id="PhQ-El-olR"/>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="bottom" secondItem="7wb-qe-IRt" secondAttribute="bottom" constant="8" id="VY4-aX-YID"/>
|
||||||
|
<constraint firstItem="sHU-GU-klv" firstAttribute="top" secondItem="DfO-uD-UNI" secondAttribute="bottom" constant="8" id="Vza-1s-qbG"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="sHU-GU-klv" secondAttribute="trailing" id="XJa-zP-Ma2"/>
|
||||||
|
<constraint firstItem="sHU-GU-klv" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leadingMargin" id="cSX-WD-2aJ"/>
|
||||||
|
<constraint firstItem="Fw7-OL-iy5" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="d1j-6d-hBb"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="LjK-72-Bez" secondAttribute="trailing" constant="16" id="hn9-c3-iNH"/>
|
||||||
|
<constraint firstItem="MIj-OR-NOR" firstAttribute="leading" secondItem="KyB-ey-l11" secondAttribute="trailing" constant="8" id="iG7-yZ-9u3"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="sHU-GU-klv" secondAttribute="bottom" constant="8" id="iRf-l0-ZZX"/>
|
||||||
|
<constraint firstItem="MIj-OR-NOR" firstAttribute="top" relation="greaterThanOrEqual" secondItem="LjK-72-Bez" secondAttribute="bottom" id="nMM-6t-bjX"/>
|
||||||
|
<constraint firstItem="DfO-uD-UNI" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="16" id="pqd-E3-Aw4"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="7wb-qe-IRt" secondAttribute="trailing" constant="8" id="qvF-dI-4qY"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="avatarContainerView" destination="KyB-ey-l11" id="45s-jV-l8L"/>
|
||||||
|
<outlet property="avatarImageView" destination="tH8-sR-DHC" id="6ll-yL-g1o"/>
|
||||||
|
<outlet property="displayNameLabel" destination="LjK-72-Bez" id="nIU-ey-H1C"/>
|
||||||
|
<outlet property="fieldNamesStackView" destination="pV2-Mz-54W" id="xfG-60-K0s"/>
|
||||||
|
<outlet property="fieldValuesStackView" destination="oza-9d-8v4" id="UIS-KM-5fR"/>
|
||||||
|
<outlet property="fieldsStackView" destination="sHU-GU-klv" id="Gli-Gf-Ubh"/>
|
||||||
|
<outlet property="followsYouLabel" destination="a32-1a-xXZ" id="phY-0L-NnN"/>
|
||||||
|
<outlet property="headerImageView" destination="Fw7-OL-iy5" id="6sv-E5-D73"/>
|
||||||
|
<outlet property="moreButton" destination="7wb-qe-IRt" id="CIB-zo-ASU"/>
|
||||||
|
<outlet property="noteTextView" destination="bnc-3t-t7t" id="dV2-7U-gSd"/>
|
||||||
|
<outlet property="usernameLabel" destination="MIj-OR-NOR" id="e1I-N7-rKx"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="40.799999999999997" y="110.64467766116942"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
||||||
|
<systemColor name="labelColor">
|
||||||
|
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="secondaryLabelColor">
|
||||||
|
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
<systemColor name="systemBackgroundColor">
|
||||||
|
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
|
@ -1,183 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17132" 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="17105"/>
|
|
||||||
<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"/>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iN0-l3-epB" customClass="ProfileHeaderView" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dgG-dR-lSv">
|
|
||||||
<rect key="frame" x="0.0" y="44" width="414" height="150"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="150" id="aCE-CA-XWm"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wT9-2J-uSY">
|
|
||||||
<rect key="frame" x="16" y="134" width="120" height="120"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="TkY-oK-if4">
|
|
||||||
<rect key="frame" x="2" y="2" width="116" height="116"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="116" id="eDg-Vc-o8R"/>
|
|
||||||
<constraint firstAttribute="width" constant="116" id="r63-z8-eBH"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="120" id="6Dt-Ml-clv"/>
|
|
||||||
<constraint firstItem="TkY-oK-if4" firstAttribute="centerY" secondItem="wT9-2J-uSY" secondAttribute="centerY" id="NYH-4p-7zT"/>
|
|
||||||
<constraint firstAttribute="width" constant="120" id="PYe-ej-mRj"/>
|
|
||||||
<constraint firstItem="TkY-oK-if4" firstAttribute="centerX" secondItem="wT9-2J-uSY" secondAttribute="centerX" id="ozz-sa-gSc"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="vcl-Gl-kXl" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="144" y="202" width="254" height="24"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bRJ-Xf-kc9" customClass="VisualEffectImageButton" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="374" y="154" width="32" height="32"/>
|
|
||||||
<viewLayoutGuide key="safeArea" id="kQa-ou-pQz"/>
|
|
||||||
<accessibility key="accessibilityConfiguration">
|
|
||||||
<accessibilityTraits key="traits" button="YES"/>
|
|
||||||
</accessibility>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="32" id="1lX-9R-x90"/>
|
|
||||||
<constraint firstAttribute="height" constant="32" id="Vp8-v8-Lr1"/>
|
|
||||||
</constraints>
|
|
||||||
<userDefinedRuntimeAttributes>
|
|
||||||
<userDefinedRuntimeAttribute type="image" keyPath="image" value="ellipsis" catalog="system"/>
|
|
||||||
</userDefinedRuntimeAttributes>
|
|
||||||
<connections>
|
|
||||||
<action selector="morePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="Td6-rw-Xvr"/>
|
|
||||||
</connections>
|
|
||||||
</view>
|
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="u4P-3i-gEq">
|
|
||||||
<rect key="frame" x="16" y="262" width="398" height="600"/>
|
|
||||||
<subviews>
|
|
||||||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Follows you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF8-nI-KVj">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="75.5" height="0.0"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" verticalHuggingPriority="251" horizontalCompressionResistancePriority="749" scrollEnabled="NO" delaysContentTouches="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1O8-2P-Gbf" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="382" height="186.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="17"/>
|
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
|
||||||
</textView>
|
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="sp4-Zb-00B">
|
|
||||||
<rect key="frame" x="0.0" y="194.5" width="382" height="358"/>
|
|
||||||
<subviews>
|
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="aqG-FA-so5">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="186.5" height="358"/>
|
|
||||||
</stackView>
|
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="EfH-Dj-Jmn">
|
|
||||||
<rect key="frame" x="194.5" y="0.0" width="187.5" height="358"/>
|
|
||||||
</stackView>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="EfH-Dj-Jmn" firstAttribute="width" relation="greaterThanOrEqual" secondItem="aqG-FA-so5" secondAttribute="width" multiplier="0.5" id="2hZ-pF-C9b"/>
|
|
||||||
<constraint firstItem="EfH-Dj-Jmn" firstAttribute="width" relation="lessThanOrEqual" secondItem="aqG-FA-so5" secondAttribute="width" multiplier="2" id="Lir-Ff-z0m"/>
|
|
||||||
</constraints>
|
|
||||||
</stackView>
|
|
||||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="n1M-vM-Cj0">
|
|
||||||
<rect key="frame" x="0.0" y="560.5" width="382" height="32"/>
|
|
||||||
<segments>
|
|
||||||
<segment title="Posts"/>
|
|
||||||
<segment title="Posts and Replies"/>
|
|
||||||
<segment title="Media"/>
|
|
||||||
</segments>
|
|
||||||
<connections>
|
|
||||||
<action selector="postsSegmentedControlChanged:" destination="iN0-l3-epB" eventType="valueChanged" id="D6y-ZM-DwU"/>
|
|
||||||
</connections>
|
|
||||||
</segmentedControl>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5ja-fK-Fqz">
|
|
||||||
<rect key="frame" x="0.0" y="599.5" width="398" height="0.5"/>
|
|
||||||
<color key="backgroundColor" systemColor="separatorColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="0.5" id="VwS-gV-q8M"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="n1M-vM-Cj0" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="9Ds-zl-acc"/>
|
|
||||||
<constraint firstItem="sp4-Zb-00B" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="Qum-qT-goH"/>
|
|
||||||
<constraint firstItem="5ja-fK-Fqz" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" id="azv-le-93y"/>
|
|
||||||
<constraint firstItem="1O8-2P-Gbf" firstAttribute="width" secondItem="u4P-3i-gEq" secondAttribute="width" constant="-16" id="hnA-3G-B9B"/>
|
|
||||||
</constraints>
|
|
||||||
</stackView>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1C3-Pd-QiL">
|
|
||||||
<rect key="frame" x="144" y="234" width="254" height="18"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
|
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="23a-no-Gjj"/>
|
|
||||||
<constraint firstItem="dgG-dR-lSv" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="6Q0-q5-Ju6"/>
|
|
||||||
<constraint firstItem="wT9-2J-uSY" firstAttribute="centerY" secondItem="dgG-dR-lSv" secondAttribute="bottom" id="7gb-T3-Xe7"/>
|
|
||||||
<constraint firstItem="vcl-Gl-kXl" firstAttribute="top" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="8" symbolic="YES" id="7ss-Mf-YYH"/>
|
|
||||||
<constraint firstItem="vcl-Gl-kXl" firstAttribute="leading" secondItem="wT9-2J-uSY" secondAttribute="trailing" constant="8" id="8ho-WU-MxW"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="u4P-3i-gEq" secondAttribute="bottom" id="9zc-N2-mfI"/>
|
|
||||||
<constraint firstItem="bRJ-Xf-kc9" firstAttribute="bottom" secondItem="dgG-dR-lSv" secondAttribute="bottom" constant="-8" id="AXS-bG-20Q"/>
|
|
||||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="bottom" secondItem="TkY-oK-if4" secondAttribute="bottom" id="OpB-YM-gyu"/>
|
|
||||||
<constraint firstItem="dgG-dR-lSv" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="VD1-yc-KSa"/>
|
|
||||||
<constraint firstItem="wT9-2J-uSY" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="WNS-AR-ff2"/>
|
|
||||||
<constraint firstItem="bRJ-Xf-kc9" firstAttribute="trailing" secondItem="vUN-kp-3ea" secondAttribute="trailing" constant="-8" id="ZB4-ys-9zP"/>
|
|
||||||
<constraint firstItem="1C3-Pd-QiL" firstAttribute="top" relation="greaterThanOrEqual" secondItem="vcl-Gl-kXl" secondAttribute="bottom" id="d0z-X6-Sig"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="vcl-Gl-kXl" secondAttribute="trailing" constant="16" id="e38-Od-kPg"/>
|
|
||||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="hgl-UR-o3W"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dgG-dR-lSv" secondAttribute="trailing" id="j0d-hY-815"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="1C3-Pd-QiL" secondAttribute="trailing" constant="16" id="pcH-vi-2zH"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="u4P-3i-gEq" secondAttribute="trailing" id="ph6-NT-A02"/>
|
|
||||||
<constraint firstItem="u4P-3i-gEq" firstAttribute="top" secondItem="wT9-2J-uSY" secondAttribute="bottom" constant="8" id="tKQ-6d-Z55"/>
|
|
||||||
</constraints>
|
|
||||||
<connections>
|
|
||||||
<outlet property="avatarContainerView" destination="wT9-2J-uSY" id="yEm-h7-tfq"/>
|
|
||||||
<outlet property="avatarImageView" destination="TkY-oK-if4" id="bSJ-7z-j4w"/>
|
|
||||||
<outlet property="displayNameLabel" destination="vcl-Gl-kXl" id="64n-a9-my0"/>
|
|
||||||
<outlet property="fieldNamesStackView" destination="aqG-FA-so5" id="prA-n7-blZ"/>
|
|
||||||
<outlet property="fieldValuesStackView" destination="EfH-Dj-Jmn" id="LMk-Hn-EkY"/>
|
|
||||||
<outlet property="fieldsStackView" destination="sp4-Zb-00B" id="eyx-GF-2Wf"/>
|
|
||||||
<outlet property="followsYouLabel" destination="UF8-nI-KVj" id="dTe-DQ-eJV"/>
|
|
||||||
<outlet property="headerImageView" destination="dgG-dR-lSv" id="HXT-v4-2iX"/>
|
|
||||||
<outlet property="moreButton" destination="bRJ-Xf-kc9" id="zIN-pz-L7y"/>
|
|
||||||
<outlet property="noteTextView" destination="1O8-2P-Gbf" id="yss-zZ-uQ5"/>
|
|
||||||
<outlet property="pagesSegmentedControl" destination="n1M-vM-Cj0" id="TCU-ku-YZN"/>
|
|
||||||
<outlet property="usernameLabel" destination="1C3-Pd-QiL" id="57b-LQ-3pM"/>
|
|
||||||
</connections>
|
|
||||||
<point key="canvasLocation" x="-590" y="117"/>
|
|
||||||
</view>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<image name="ellipsis" catalog="system" width="128" height="37"/>
|
|
||||||
<systemColor name="labelColor">
|
|
||||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="secondaryLabelColor">
|
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" 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="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
|
@ -378,7 +378,7 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
|
||||||
guard let mastodonController = mastodonController else { return nil }
|
guard let mastodonController = mastodonController else { return nil }
|
||||||
if avatarImageView.frame.contains(location) {
|
if avatarImageView.frame.contains(location) {
|
||||||
return (
|
return (
|
||||||
content: { ProfileViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
content: { ProfileTableViewController(accountID: self.accountID, mastodonController: mastodonController) },
|
||||||
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
actions: { self.actionsForProfile(accountID: self.accountID, sourceView: self.avatarImageView) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -252,7 +252,7 @@ struct XCBActions {
|
||||||
static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) {
|
||||||
getAccount(from: request, session: session) { (account) in
|
getAccount(from: request, session: session) { (account) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController)
|
let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController)
|
||||||
show(vc)
|
show(vc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,7 +307,7 @@ struct XCBActions {
|
||||||
if silent ?? false {
|
if silent ?? false {
|
||||||
performAction(account)
|
performAction(account)
|
||||||
} else {
|
} else {
|
||||||
let vc = ProfileViewController(accountID: account.id, mastodonController: mastodonController)
|
let vc = ProfileTableViewController(accountID: account.id, mastodonController: mastodonController)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
show(vc)
|
show(vc)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue