Compare commits

..

No commits in common. "e3cc0df28323e949afcfe7f823cae8f4060e7430" and "0691c3b9d65d6cd9b4b082030337c81dc02a1cf6" have entirely different histories.

27 changed files with 264 additions and 157 deletions

View File

@ -29,15 +29,15 @@ public class Attachment: Decodable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
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 = URL(string: try container.decode(String.self, forKey: .url))! self.url = URL(lenient: try container.decode(String.self, forKey: .url))!
self.previewURL = URL(string: try container.decode(String.self, forKey: .previewURL))!
if let remote = try? container.decode(String.self, forKey: .remoteURL) { if let remote = try? container.decode(String.self, forKey: .remoteURL) {
self.remoteURL = URL(string: remote)! self.remoteURL = URL(lenient: remote.replacingOccurrences(of: " ", with: "%20"))
} else { } else {
self.remoteURL = nil self.remoteURL = nil
} }
self.previewURL = URL(lenient: try container.decode(String.self, forKey: .previewURL).replacingOccurrences(of: " ", with: "%20"))!
if let text = try? container.decode(String.self, forKey: .textURL) { if let text = try? container.decode(String.self, forKey: .textURL) {
self.textURL = URL(string: text)! self.textURL = URL(lenient: text.replacingOccurrences(of: " ", with: "%20"))
} else { } else {
self.textURL = nil self.textURL = nil
} }
@ -113,3 +113,14 @@ extension Attachment {
} }
} }
} }
fileprivate extension URL {
private static let allowedChars = CharacterSet.urlHostAllowed.union(.urlPathAllowed).union(.urlQueryAllowed)
init?(lenient string: String) {
guard let escaped = string.addingPercentEncoding(withAllowedCharacters: URL.allowedChars) else {
return nil
}
self.init(string: escaped)
}
}

View File

@ -25,6 +25,7 @@
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; }; 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; }; D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6028B9A2150811100F223B9 /* MastodonCache.swift */; };
D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; }; D60309B52419D4F100A465FF /* ComposeAttachmentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */; };
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */; };
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; }; D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */; };
D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; }; D61099B42144B0CC00432DC2 /* Pachyderm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61099AB2144B0CC00432DC2 /* Pachyderm.framework */; };
D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; }; D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61099BA2144B0CC00432DC2 /* PachydermTests.swift */; };
@ -105,6 +106,7 @@
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; }; D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; }; D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; }; D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; }; D63569E023908A8D003DD353 /* StatusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60A4FFB238B726A008AC647 /* StatusState.swift */; };
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; }; D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */; };
@ -309,6 +311,7 @@
D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; }; D6028B9A2150811100F223B9 /* MastodonCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonCache.swift; sourceTree = "<group>"; };
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = "<group>"; }; D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentsViewController.swift; sourceTree = "<group>"; };
D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; }; D60A4FFB238B726A008AC647 /* StatusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusState.swift; sourceTree = "<group>"; };
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeMediaView.xib; sourceTree = "<group>"; };
D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; }; D60D2B8123844C71001B87A3 /* BaseStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStatusTableViewCell.swift; sourceTree = "<group>"; };
D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D61099AB2144B0CC00432DC2 /* Pachyderm.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pachyderm.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; }; D61099AD2144B0CC00432DC2 /* Pachyderm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Pachyderm.h; sourceTree = "<group>"; };
@ -390,6 +393,7 @@
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; }; D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; }; D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; }; D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; }; D63661BF2381C144004B9E16 /* PreferencesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesNavigationController.swift; sourceTree = "<group>"; };
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddAttachmentTableViewCell.xib; sourceTree = "<group>"; }; D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddAttachmentTableViewCell.xib; sourceTree = "<group>"; };
@ -599,6 +603,15 @@
path = Transitions; path = Transitions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D60C07E221E817560057FAA8 /* Compose Media */ = {
isa = PBXGroup;
children = (
D60C07E321E8176B0057FAA8 /* ComposeMediaView.xib */,
D6333B762138D94E00CE884A /* ComposeMediaView.swift */,
);
path = "Compose Media";
sourceTree = "<group>";
};
D61099AC2144B0CC00432DC2 /* Pachyderm */ = { D61099AC2144B0CC00432DC2 /* Pachyderm */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -694,37 +707,6 @@
path = "Hashtag Cell"; path = "Hashtag Cell";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D61959D0241E842400A37B8E /* Draft Cell */ = {
isa = PBXGroup;
children = (
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
);
path = "Draft Cell";
sourceTree = "<group>";
};
D61959D1241E844900A37B8E /* Attachment Cells */ = {
isa = PBXGroup;
children = (
D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */,
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */,
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */,
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */,
);
path = "Attachment Cells";
sourceTree = "<group>";
};
D61959D2241E846D00A37B8E /* Models */ = {
isa = PBXGroup;
children = (
D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
D620483123D2A6A3008A63EF /* CompositionState.swift */,
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */,
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
);
path = Models;
sourceTree = "<group>";
};
D61AC1DA232EA43100C54D2D /* Instance Cell */ = { D61AC1DA232EA43100C54D2D /* Instance Cell */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -792,6 +774,8 @@
children = ( children = (
D627FF78217E950100CC0648 /* DraftsTableViewController.xib */, D627FF78217E950100CC0648 /* DraftsTableViewController.xib */,
D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */, D627FF7A217E951500CC0648 /* DraftsTableViewController.swift */,
D627FF7C217E958900CC0648 /* DraftTableViewCell.xib */,
D627FF7E217E95E000CC0648 /* DraftTableViewCell.swift */,
); );
path = Drafts; path = Drafts;
sourceTree = "<group>"; sourceTree = "<group>";
@ -807,6 +791,20 @@
path = Shortcuts; path = Shortcuts;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D63F9C64241C4CAA004C03CF /* Attachments */ = {
isa = PBXGroup;
children = (
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */,
D626493423BD94CE00612E6E /* CompositionAttachmentData.swift */,
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
D63F9C67241C4F79004C03CF /* AddAttachmentTableViewCell.swift */,
D63F9C65241C4CC3004C03CF /* AddAttachmentTableViewCell.xib */,
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */,
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */,
);
path = Attachments;
sourceTree = "<group>";
};
D641C780213DD7C4004B4513 /* Screens */ = { D641C780213DD7C4004B4513 /* Screens */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -818,8 +816,6 @@
D641C785213DD83B004B4513 /* Conversation */, D641C785213DD83B004B4513 /* Conversation */,
D641C786213DD852004B4513 /* Notifications */, D641C786213DD852004B4513 /* Notifications */,
D641C787213DD862004B4513 /* Compose */, D641C787213DD862004B4513 /* Compose */,
D6B053A023BD2BED00A066FA /* Asset Picker */,
D627FF77217E94F200CC0648 /* Drafts */,
D627943C23A5635D00D38C68 /* Explore */, D627943C23A5635D00D38C68 /* Explore */,
D6BC9DD8232D8BCA002CA326 /* Search */, D6BC9DD8232D8BCA002CA326 /* Search */,
D627944B23A9A02400D38C68 /* Lists */, D627944B23A9A02400D38C68 /* Lists */,
@ -891,9 +887,13 @@
D641C787213DD862004B4513 /* Compose */ = { D641C787213DD862004B4513 /* Compose */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D6B053A023BD2BED00A066FA /* Asset Picker */,
D63F9C64241C4CAA004C03CF /* Attachments */,
D627FF77217E94F200CC0648 /* Drafts */,
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */, D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */, D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */, D6285B5221EA708700FE4B39 /* StatusFormat.swift */,
D620483123D2A6A3008A63EF /* CompositionState.swift */,
); );
path = Compose; path = Compose;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1155,9 +1155,8 @@
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */, D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
D67C57A721E2649B00C3118B /* Account Detail */, D67C57A721E2649B00C3118B /* Account Detail */,
D67C57B021E28F9400C3118B /* Compose Status Reply */, D67C57B021E28F9400C3118B /* Compose Status Reply */,
D60C07E221E817560057FAA8 /* Compose Media */,
D626494023C122C800612E6E /* Asset Picker */, D626494023C122C800612E6E /* Asset Picker */,
D61959D1241E844900A37B8E /* Attachment Cells */,
D61959D0241E842400A37B8E /* Draft Cell */,
D641C78A213DD926004B4513 /* Status */, D641C78A213DD926004B4513 /* Status */,
D6C7D27B22B6EBE200071952 /* Attachments */, D6C7D27B22B6EBE200071952 /* Attachments */,
D641C78B213DD92F004B4513 /* Profile Header */, D641C78B213DD92F004B4513 /* Profile Header */,
@ -1238,7 +1237,6 @@
D663626021360A9600C9CBA2 /* Preferences */, D663626021360A9600C9CBA2 /* Preferences */,
D6AEBB3F2321640F00E5038B /* Activities */, D6AEBB3F2321640F00E5038B /* Activities */,
D667E5F62135C2ED0057A976 /* Extensions */, D667E5F62135C2ED0057A976 /* Extensions */,
D61959D2241E846D00A37B8E /* Models */,
D6F953F121251A2F00CF0F2B /* Controllers */, D6F953F121251A2F00CF0F2B /* Controllers */,
D641C780213DD7C4004B4513 /* Screens */, D641C780213DD7C4004B4513 /* Screens */,
D6BED1722126661300F02DA0 /* Views */, D6BED1722126661300F02DA0 /* Views */,
@ -1511,6 +1509,7 @@
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */, D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */,
D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */,
D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */, D63F9C66241C4CC3004C03CF /* AddAttachmentTableViewCell.xib in Resources */,
D60C07E421E8176B0057FAA8 /* ComposeMediaView.xib in Resources */,
D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */,
D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */, D6A5FAF1217B7E05003DB2D9 /* ComposeViewController.xib in Resources */,
); );
@ -1672,6 +1671,7 @@
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */, D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */, D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */, D6AEBB4823216B1D00E5038B /* AccountActivity.swift in Sources */,
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */, D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,

View File

@ -82,9 +82,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// This occurs shortly after the scene enters the background, or when its session is discarded. // This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects. // Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
Preferences.save()
DraftsManager.save()
} }
func sceneDidBecomeActive(_ scene: UIScene) { func sceneDidBecomeActive(_ scene: UIScene) {
@ -95,9 +92,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneWillResignActive(_ scene: UIScene) { func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state. // Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call). // This may occur due to temporary interruptions (ex. an incoming phone call).
Preferences.save()
DraftsManager.save()
} }
func sceneWillEnterForeground(_ scene: UIScene) { func sceneWillEnterForeground(_ scene: UIScene) {
@ -109,6 +103,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Called as the scene transitions from the foreground to the background. // Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information // Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state. // to restore the scene back to its current state.
Preferences.save()
DraftsManager.save()
} }
func activateAccount(_ account: LocalData.UserAccountInfo) { func activateAccount(_ account: LocalData.UserAccountInfo) {

View File

@ -177,11 +177,10 @@ class AssetCollectionViewController: UICollectionViewController {
} }
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?, if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?, let cell = collectionView.cellForItem(at: indexPath) {
let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell {
let parameters = UIPreviewParameters() let parameters = UIPreviewParameters()
parameters.backgroundColor = .black parameters.backgroundColor = .black
return UITargetedPreview(view: cell.imageView, parameters: parameters) return UITargetedPreview(view: cell, parameters: parameters)
} else { } else {
return nil return nil
} }

View File

@ -13,18 +13,14 @@ import AVKit
class AssetPreviewViewController: UIViewController { class AssetPreviewViewController: UIViewController {
let attachment: CompositionAttachmentData let asset: PHAsset
init(attachment: CompositionAttachmentData) { init(asset: PHAsset) {
self.attachment = attachment self.asset = asset
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
convenience init(asset: PHAsset) {
self.init(attachment: .asset(asset))
}
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -34,29 +30,21 @@ class AssetPreviewViewController: UIViewController {
view.backgroundColor = .black view.backgroundColor = .black
switch attachment { if asset.mediaType == .image {
case let .image(image): if asset.mediaSubtypes.contains(.photoLive) {
showImage(image) showLivePhoto()
case let .video(url): } else {
showVideo(asset: AVURLAsset(url: url)) showImage()
case let .asset(asset):
switch asset.mediaType {
case .image:
if asset.mediaSubtypes.contains(.photoLive) {
showLivePhoto(asset)
} else {
showAssetImage(asset)
}
case .video:
showAssetVideo(asset)
default:
fatalError("asset mediaType must be image or video")
} }
} else if asset.mediaType == .video {
playVideo()
} else {
fatalError("asset mediaType must be image or video")
} }
} }
func showImage(_ image: UIImage) { func showImage() {
let imageView = UIImageView(image: image) let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit imageView.contentMode = .scaleAspectFit
view.addSubview(imageView) view.addSubview(imageView)
@ -66,10 +54,7 @@ class AssetPreviewViewController: UIViewController {
imageView.topAnchor.constraint(equalTo: view.topAnchor), imageView.topAnchor.constraint(equalTo: view.topAnchor),
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor) imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]) ])
preferredContentSize = image.size
}
func showAssetImage(_ asset: PHAsset) {
let options = PHImageRequestOptions() let options = PHImageRequestOptions()
options.version = .current options.version = .current
options.deliveryMode = .opportunistic options.deliveryMode = .opportunistic
@ -77,12 +62,13 @@ class AssetPreviewViewController: UIViewController {
options.isNetworkAccessAllowed = true options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in PHImageManager.default().requestImage(for: asset, targetSize: view.bounds.size, contentMode: .aspectFit, options: options) { (image, _) in
DispatchQueue.main.async { DispatchQueue.main.async {
self.showImage(image!) imageView.image = image
self.preferredContentSize = image!.size
} }
} }
} }
func showLivePhoto(_ asset: PHAsset) { func showLivePhoto() {
let options = PHLivePhotoRequestOptions() let options = PHLivePhotoRequestOptions()
options.deliveryMode = .opportunistic options.deliveryMode = .opportunistic
options.version = .current options.version = .current
@ -110,18 +96,7 @@ class AssetPreviewViewController: UIViewController {
} }
} }
func showVideo(asset: AVAsset) { func playVideo() {
let playerController = AVPlayerViewController()
let item = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: item)
player.isMuted = true
player.play()
playerController.player = player
self.embedChild(playerController)
self.preferredContentSize = item.presentationSize
}
func showAssetVideo(_ asset: PHAsset) {
let options = PHVideoRequestOptions() let options = PHVideoRequestOptions()
options.deliveryMode = .automatic options.deliveryMode = .automatic
options.isNetworkAccessAllowed = true options.isNetworkAccessAllowed = true
@ -131,7 +106,14 @@ class AssetPreviewViewController: UIViewController {
fatalError("failed to get AVAsset") fatalError("failed to get AVAsset")
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.showVideo(asset: avAsset) let playerController = AVPlayerViewController()
let item = AVPlayerItem(asset: avAsset)
let player = AVPlayer(playerItem: item)
player.isMuted = true
player.play()
playerController.player = player
self.embedChild(playerController)
self.preferredContentSize = item.presentationSize
} }
} }
} }

View File

@ -82,8 +82,8 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
extension ComposeAttachmentTableViewCell: UITextViewDelegate { extension ComposeAttachmentTableViewCell: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) { func textViewDidChange(_ textView: UITextView) {
delegate?.attachmentDescriptionChanged(self)
attachment.attachmentDescription = textView.text attachment.attachmentDescription = textView.text
updateDescriptionPlaceholderLabel() updateDescriptionPlaceholderLabel()
delegate?.attachmentDescriptionChanged(self)
} }
} }

View File

@ -25,14 +25,13 @@ class ComposeAttachmentsViewController: UITableViewController {
var attachments: [CompositionAttachment] = [] { var attachments: [CompositionAttachment] = [] {
didSet { didSet {
delegate?.composeSelectedAttachmentsDidChange() delegate?.composeSelectedAttachmentsDidChange()
delegate?.composeRequiresAttachmentDescriptionsDidChange()
updateAddAttachmentsButtonEnabled() updateAddAttachmentsButtonEnabled()
} }
} }
var requiresAttachmentDescriptions: Bool { var requiresAttachmentDescriptions: Bool {
if Preferences.shared.requireAttachmentDescriptions { if Preferences.shared.requireAttachmentDescriptions {
return attachments.contains { $0.attachmentDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } return !attachments.allSatisfy { $0.description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
} else { } else {
return false return false
} }
@ -265,37 +264,6 @@ class ComposeAttachmentsViewController: UITableViewController {
} }
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let attachment = attachments[indexPath.row]
// cast to NSIndexPath because identifier needs to conform to NSCopying
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
return AssetPreviewViewController(attachment: attachment.data)
}) { (_) -> UIMenu? in
return nil
}
}
private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
let parameters = UIPreviewParameters()
parameters.backgroundColor = .black
return UITargetedPreview(view: cell.assetImageView, parameters: parameters)
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
return targetedPreview(forConfiguration: configuration)
}
override func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
return targetedPreview(forConfiguration: configuration)
}
// MARK: Interaction
func addAttachmentPressed() { func addAttachmentPressed() {
let sheetContainer = AssetPickerSheetContainerViewController() let sheetContainer = AssetPickerSheetContainerViewController()
sheetContainer.assetPicker.assetPickerDelegate = self sheetContainer.assetPicker.assetPickerDelegate = self

View File

@ -7,7 +7,6 @@
// //
import UIKit import UIKit
import SwiftSoup
class AccountTableViewCell: UITableViewCell { class AccountTableViewCell: UITableViewCell {
@ -17,7 +16,6 @@ class AccountTableViewCell: UITableViewCell {
@IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: EmojiLabel! @IBOutlet weak var displayNameLabel: EmojiLabel!
@IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var usernameLabel: UILabel!
@IBOutlet weak var noteLabel: EmojiLabel!
var accountID: String! var accountID: String!
@ -27,7 +25,7 @@ class AccountTableViewCell: UITableViewCell {
super.awakeFromNib() super.awakeFromNib()
avatarImageView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPrefrences), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPrefrences), name: .preferencesChanged, object: nil)
} }
@ -56,10 +54,6 @@ class AccountTableViewCell: UITableViewCell {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
let doc = try! SwiftSoup.parse(account.note)
noteLabel.text = try! doc.text()
noteLabel.setEmojis(account.emojis, identifier: account.id)
updateUIForPrefrences() updateUIForPrefrences()
} }

View File

@ -9,45 +9,40 @@
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="100" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target"> <tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="AccountTableViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/> <rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/> <rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Rp2-O5-Vew">
<rect key="frame" x="16" y="8" width="50" height="50"/> <rect key="frame" x="16" y="8" width="50" height="50"/>
<constraints> <constraints>
<constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/> <constraint firstAttribute="width" secondItem="Rp2-O5-Vew" secondAttribute="height" multiplier="1:1" id="1AQ-lU-ptd"/>
<constraint firstAttribute="height" constant="50" id="NqI-m0-owe"/> <constraint firstAttribute="height" priority="999" constant="50" id="NqI-m0-owe"/>
</constraints> </constraints>
</imageView> </imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Iif-9m-vM5">
<rect key="frame" x="74" y="11" width="230" height="78"/> <rect key="frame" x="74" y="11" width="230" height="44"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Display Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fhc-bZ-lkB" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="230" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="230" height="26"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="@username" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JMo-QH-1is">
<rect key="frame" x="0.0" y="20.5" width="230" height="18"/> <rect key="frame" x="0.0" y="26" width="230" height="18"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/> <fontDescription key="fontDescription" type="system" weight="light" pointSize="15"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Note" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bNO-qR-YEe" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="38.5" width="230" height="39.5"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
</stackView> </stackView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstAttribute="bottom" secondItem="Rp2-O5-Vew" secondAttribute="bottom" constant="8" id="Vw1-OF-tnw"/>
<constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/> <constraint firstAttribute="bottomMargin" secondItem="Iif-9m-vM5" secondAttribute="bottom" id="dV0-Vm-DUb"/>
<constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/> <constraint firstItem="Iif-9m-vM5" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="topMargin" id="ihr-er-kLO"/>
<constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/> <constraint firstAttribute="trailingMargin" secondItem="Iif-9m-vM5" secondAttribute="trailing" id="q7a-DT-WPF"/>
@ -60,10 +55,9 @@
<connections> <connections>
<outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/> <outlet property="avatarImageView" destination="Rp2-O5-Vew" id="3Gw-Xg-bd5"/>
<outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/> <outlet property="displayNameLabel" destination="Fhc-bZ-lkB" id="1b0-3k-KR8"/>
<outlet property="noteLabel" destination="bNO-qR-YEe" id="4oO-c0-BOT"/>
<outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/> <outlet property="usernameLabel" destination="JMo-QH-1is" id="ElX-ua-xcQ"/>
</connections> </connections>
<point key="canvasLocation" x="173.91304347826087" y="35.491071428571423"/> <point key="canvasLocation" x="173.91304347826087" y="24.107142857142858"/>
</tableViewCell> </tableViewCell>
</objects> </objects>
</document> </document>

View File

@ -0,0 +1,75 @@
//
// ComposeAttachmentView.swift
// Tusker
//
// Created by Shadowfacts on 1/10/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
import Photos
import AVFoundation
protocol ComposeMediaViewDelegate: class {
func didRemoveMedia(_ mediaView: ComposeMediaView)
func descriptionTextViewDidChange(_ mediaView: ComposeMediaView)
}
class ComposeMediaView: UIView {
weak var delegate: ComposeMediaViewDelegate?
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var descriptionTextView: UITextView!
@IBOutlet weak var placeholderLabel: UILabel!
var attachment: CompositionAttachmentData!
static func create() -> ComposeMediaView {
return UINib(nibName: "ComposeMediaView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! ComposeMediaView
}
override func awakeFromNib() {
super.awakeFromNib()
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 10 // 0.1 * imageView.frame.width
descriptionTextView.delegate = self
}
func update(attachment: CompositionAttachmentData) {
self.attachment = attachment
switch attachment {
case let .image(image):
imageView.image = image
case let .asset(asset):
let size = CGSize(width: 80, height: 80)
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
guard self.attachment == attachment else { return }
self.imageView.image = image
}
case let .video(url):
let asset = AVURLAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
imageView.image = UIImage(cgImage: cgImage)
}
}
}
// MARK: - Interaction
@IBAction func removePressed(_ sender: Any) {
delegate?.didRemoveMedia(self)
}
}
extension ComposeMediaView: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
placeholderLabel.isHidden = !descriptionTextView.text.isEmpty
delegate?.descriptionTextViewDidChange(self)
}
}

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.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="ComposeMediaView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="u7I-sx-kUe">
<rect key="frame" x="8" y="293.5" width="80" height="80"/>
<constraints>
<constraint firstAttribute="height" constant="80" id="CgF-eC-We6"/>
<constraint firstAttribute="width" constant="80" id="S3g-yM-TRb"/>
</constraints>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="G1g-Lw-Ren">
<rect key="frame" x="345" y="322.5" width="22" height="22"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="dyG-5Y-91s"/>
<constraint firstAttribute="width" constant="22" id="sWQ-3z-Z5r"/>
</constraints>
<state key="normal" image="xmark.circle.fill" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state>
<connections>
<action selector="removePressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="aor-Cq-YjJ"/>
</connections>
</button>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="O6b-Zs-u8r">
<rect key="frame" x="96" y="0.0" width="241" height="667"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="80" id="GsE-uM-fhe"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Describe for the visually impaired..." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rkD-NP-09H">
<rect key="frame" x="100" y="8" width="233" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0h4-wv-2R8">
<rect key="frame" x="0.0" y="0.0" width="375" height="1"/>
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="aQa-2T-uYY"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="O6b-Zs-u8r" secondAttribute="bottom" id="3sv-wo-gxe"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="5Qs-i7-glv"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="top" relation="greaterThanOrEqual" secondItem="0h4-wv-2R8" secondAttribute="bottom" constant="8" id="86L-Cb-Lsk"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="9QY-MR-Yc2"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0h4-wv-2R8" secondAttribute="trailing" id="FCS-un-JT6"/>
<constraint firstItem="u7I-sx-kUe" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="UWq-Lf-zGB"/>
<constraint firstItem="rkD-NP-09H" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="leading" constant="4" id="aQd-T9-n1I"/>
<constraint firstItem="0h4-wv-2R8" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="edp-4a-YGq"/>
<constraint firstItem="G1g-Lw-Ren" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="hJA-JF-MHp"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="leading" secondItem="u7I-sx-kUe" secondAttribute="trailing" constant="8" id="hvZ-m4-TOV"/>
<constraint firstItem="G1g-Lw-Ren" firstAttribute="leading" secondItem="O6b-Zs-u8r" secondAttribute="trailing" constant="8" id="ith-0X-3Yz"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="u7I-sx-kUe" secondAttribute="bottom" constant="8" id="lGN-Qg-mBO"/>
<constraint firstItem="rkD-NP-09H" firstAttribute="top" secondItem="O6b-Zs-u8r" secondAttribute="top" constant="8" id="mRb-3M-7uz"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="G1g-Lw-Ren" secondAttribute="trailing" constant="8" id="o4F-MV-ahd"/>
<constraint firstItem="0h4-wv-2R8" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="sFo-wp-MYP"/>
<constraint firstItem="O6b-Zs-u8r" firstAttribute="trailing" secondItem="rkD-NP-09H" secondAttribute="trailing" constant="4" id="vwg-7l-8ca"/>
</constraints>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="descriptionTextView" destination="O6b-Zs-u8r" id="TNy-7h-0sY"/>
<outlet property="imageView" destination="u7I-sx-kUe" id="o3a-O0-m85"/>
<outlet property="placeholderLabel" destination="rkD-NP-09H" id="WtV-2h-L3n"/>
</connections>
<point key="canvasLocation" x="136.80000000000001" y="142.57871064467767"/>
</view>
</objects>
<resources>
<image name="xmark.circle.fill" catalog="system" width="64" height="60"/>
</resources>
</document>

View File

@ -101,10 +101,7 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
} }
func updateTimestamp() { func updateTimestamp() {
// if the mastodonController is nil (i.e. the delegate is nil), then the screen this cell was a part of has been deallocated guard let mastodonController = mastodonController, let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
// so we bail out immediately, since there's nothing to update
guard let mastodonController = mastodonController else { return }
guard let status = mastodonController.cache.status(for: statusID) else { fatalError("Missing cached status \(statusID!)") }
timestampLabel.text = status.createdAt.timeAgoString() timestampLabel.text = status.createdAt.timeAgoString()
timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date()) timestampLabel.accessibilityLabel = TimelineStatusTableViewCell.relativeDateFormatter.localizedString(for: status.createdAt, relativeTo: Date())