Compare commits
4 Commits
89b35fab6d
...
348c306858
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 348c306858 | |
Shadowfacts | 0a11d2de47 | |
Shadowfacts | 4ac76ab672 | |
Shadowfacts | eb4e6e32f7 |
|
@ -76,6 +76,7 @@ public class Client {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let result = try? Client.decoder.decode(Result.self, from: data) else {
|
guard let result = try? Client.decoder.decode(Result.self, from: data) else {
|
||||||
|
print(request)
|
||||||
completion(.failure(.invalidModel))
|
completion(.failure(.invalidModel))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -90,7 +91,7 @@ public class Client {
|
||||||
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
|
||||||
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
|
||||||
components.path = request.path
|
components.path = request.path
|
||||||
components.queryItems = request.queryParameters.queryItems
|
components.queryItems = request.queryParameters.isEmpty ? nil : request.queryParameters.queryItems
|
||||||
guard let url = components.url else { return nil }
|
guard let url = components.url else { return nil }
|
||||||
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
|
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
|
||||||
urlRequest.httpMethod = request.method.name
|
urlRequest.httpMethod = request.method.name
|
||||||
|
|
|
@ -231,6 +231,8 @@
|
||||||
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
|
D6B053AB23BD2F1400A066FA /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */; };
|
||||||
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
|
D6B053AC23BD2F1400A066FA /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */; };
|
||||||
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
|
D6B053AE23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */; };
|
||||||
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */; };
|
||||||
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */; };
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */; };
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||||
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; };
|
D6BC874521961F73006163F1 /* Gifu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6BC874421961F73006163F1 /* Gifu.framework */; };
|
||||||
|
@ -280,8 +282,10 @@
|
||||||
D6E426B9253382B300C02E1C /* SearchResultType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B8253382B300C02E1C /* SearchResultType.swift */; };
|
D6E426B9253382B300C02E1C /* SearchResultType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B8253382B300C02E1C /* SearchResultType.swift */; };
|
||||||
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; };
|
||||||
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; };
|
||||||
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||||
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */; };
|
||||||
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; };
|
||||||
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; };
|
||||||
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
D6F1F84D2193B56E00F5FE67 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F1F84C2193B56E00F5FE67 /* Cache.swift */; };
|
||||||
|
@ -569,6 +573,8 @@
|
||||||
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6B053A923BD2F1400A066FA /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
|
D6B053AA23BD2F1400A066FA /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
|
D6B053AD23BD322B00A066FA /* AssetPickerSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerSheetContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
||||||
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
D6BC874421961F73006163F1 /* Gifu.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Gifu.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -624,8 +630,10 @@
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||||
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
|
D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = "<group>"; };
|
||||||
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
|
D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = "<group>"; };
|
||||||
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||||
|
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = "<group>"; };
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = "<group>"; };
|
||||||
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
D6F1F84C2193B56E00F5FE67 /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1047,6 +1055,7 @@
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||||
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
D68015412401A74600D6103B /* MediaPrefsView.swift */,
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */,
|
||||||
|
@ -1066,6 +1075,7 @@
|
||||||
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
|
D663625C2135C74800C9CBA2 /* ConversationMainStatusTableViewCell.xib */,
|
||||||
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
|
D663625E2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift */,
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */,
|
||||||
|
D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */,
|
||||||
);
|
);
|
||||||
path = Status;
|
path = Status;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1301,6 +1311,7 @@
|
||||||
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
D6B4A4FE2506B81A000C81C1 /* AccountDisplayNameLabel.swift */,
|
||||||
D6E426802532814100C02E1C /* MaybeLazyStack.swift */,
|
D6E426802532814100C02E1C /* MaybeLazyStack.swift */,
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */,
|
||||||
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
D61959D0241E842400A37B8E /* Draft Cell */,
|
D61959D0241E842400A37B8E /* Draft Cell */,
|
||||||
|
@ -1385,6 +1396,7 @@
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
D6F1F84E2193B9BE00F5FE67 /* Caching */,
|
||||||
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
D6757A7A2157E00100721E32 /* XCallbackURL */,
|
||||||
|
@ -1842,6 +1854,7 @@
|
||||||
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */,
|
||||||
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
D677284A24ECBDF400C732D3 /* ComposeCurrentAccount.swift in Sources */,
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */,
|
||||||
|
D6B30E09254BAF63009CAEE5 /* ImageGrayscalifier.swift in Sources */,
|
||||||
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */,
|
D6E426812532814100C02E1C /* MaybeLazyStack.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
||||||
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
D6A3BC852321F6C100FD64D5 /* AccountListTableViewController.swift in Sources */,
|
||||||
|
@ -1866,6 +1879,7 @@
|
||||||
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
D677284C24ECBE9100C732D3 /* ComposeAvatarImageView.swift in Sources */,
|
||||||
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
|
||||||
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
D627FF7F217E95E000CC0648 /* DraftTableViewCell.swift in Sources */,
|
||||||
|
D6B17255254F88B800128392 /* OppositeCollapseKeywordsView.swift in Sources */,
|
||||||
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
D6DF95C12533F5DE0027A9B6 /* RelationshipMO.swift in Sources */,
|
||||||
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */,
|
||||||
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
D6AEBB4A23216F0400E5038B /* UnfollowAccountActivity.swift in Sources */,
|
||||||
|
@ -1874,8 +1888,10 @@
|
||||||
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
|
||||||
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
D6DD353F22F502EC00A9563A /* Preferences+Notification.swift in Sources */,
|
||||||
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
D64BC19223C271D9000D0238 /* MastodonActivity.swift in Sources */,
|
||||||
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
|
||||||
|
|
|
@ -23,8 +23,23 @@ extension StatusState {
|
||||||
|
|
||||||
let contentWarningCollapsible = !status.spoilerText.isEmpty
|
let contentWarningCollapsible = !status.spoilerText.isEmpty
|
||||||
|
|
||||||
|
let collapseDueToContentWarning: Bool?
|
||||||
|
if contentWarningCollapsible {
|
||||||
|
let lowercased = status.spoilerText.lowercased()
|
||||||
|
let opposite = Preferences.shared.oppositeCollapseKeywords.contains { lowercased.contains($0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()) }
|
||||||
|
|
||||||
|
if Preferences.shared.expandAllContentWarnings {
|
||||||
|
collapseDueToContentWarning = opposite
|
||||||
|
} else {
|
||||||
|
collapseDueToContentWarning = !opposite
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collapseDueToContentWarning = nil
|
||||||
|
}
|
||||||
|
|
||||||
self.collapsible = contentWarningCollapsible || longEnoughToCollapse
|
self.collapsible = contentWarningCollapsible || longEnoughToCollapse
|
||||||
self.collapsed = longEnoughToCollapse || (!Preferences.shared.expandAllContentWarnings && contentWarningCollapsible)
|
// use ?? instead of || because the content warnig pref takes priority over length
|
||||||
|
self.collapsed = collapseDueToContentWarning ?? longEnoughToCollapse
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
//
|
||||||
|
// ImageGrayscalifier.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/29/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ImageGrayscalifier {
|
||||||
|
static let queue = DispatchQueue(label: "ImageGrayscalifier", qos: .default)
|
||||||
|
|
||||||
|
private static let context = CIContext()
|
||||||
|
private static let cache = NSCache<NSURL, UIImage>()
|
||||||
|
|
||||||
|
static func convert(url: URL?, data: Data) -> UIImage? {
|
||||||
|
if let url = url,
|
||||||
|
let cached = cache.object(forKey: url as NSURL) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let source = CIImage(data: data) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return doConvert(source, url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func convert(url: URL?, cgImage: CGImage) -> UIImage? {
|
||||||
|
if let url = url,
|
||||||
|
let cached = cache.object(forKey: url as NSURL) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
return doConvert(CIImage(cgImage: cgImage), url: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func doConvert(_ source: CIImage, url: URL?) -> UIImage? {
|
||||||
|
guard let filter = CIFilter(name: "CIColorMonochrome") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.setValue(source, forKey: "inputImage")
|
||||||
|
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
||||||
|
filter.setValue(1.0, forKey: "inputIntensity")
|
||||||
|
|
||||||
|
guard let output = filter.outputImage,
|
||||||
|
let cgImage = context.createCGImage(output, from: output.extent) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
|
|
||||||
|
if let url = url {
|
||||||
|
cache.setObject(image, forKey: url as NSURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,15 +55,13 @@ class Preferences: Codable, ObservableObject {
|
||||||
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
self.openLinksInApps = try container.decode(Bool.self, forKey: .openLinksInApps)
|
||||||
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
self.useInAppSafari = try container.decode(Bool.self, forKey: .useInAppSafari)
|
||||||
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
self.inAppSafariAutomaticReaderMode = try container.decode(Bool.self, forKey: .inAppSafariAutomaticReaderMode)
|
||||||
if container.contains(.expandAllContentWarnings) {
|
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false
|
||||||
self.expandAllContentWarnings = try container.decode(Bool.self, forKey: .expandAllContentWarnings)
|
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true
|
||||||
}
|
self.oppositeCollapseKeywords = try container.decodeIfPresent([String].self, forKey: .oppositeCollapseKeywords) ?? []
|
||||||
if container.contains(.collapseLongPosts) {
|
|
||||||
self.collapseLongPosts = try container.decode(Bool.self, forKey: .collapseLongPosts)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
self.showFavoriteAndReblogCounts = try container.decode(Bool.self, forKey: .showFavoriteAndReblogCounts)
|
||||||
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
self.defaultNotificationsMode = try container.decode(NotificationsMode.self, forKey: .defaultNotificationsType)
|
||||||
|
self.grayscaleImages = try container.decodeIfPresent(Bool.self, forKey: .grayscaleImages) ?? false
|
||||||
|
|
||||||
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
self.silentActions = try container.decode([String: Permission].self, forKey: .silentActions)
|
||||||
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
self.statusContentType = try container.decode(StatusContentType.self, forKey: .statusContentType)
|
||||||
|
@ -92,9 +90,11 @@ class Preferences: Codable, ObservableObject {
|
||||||
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
try container.encode(inAppSafariAutomaticReaderMode, forKey: .inAppSafariAutomaticReaderMode)
|
||||||
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
try container.encode(expandAllContentWarnings, forKey: .expandAllContentWarnings)
|
||||||
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
try container.encode(collapseLongPosts, forKey: .collapseLongPosts)
|
||||||
|
try container.encode(oppositeCollapseKeywords, forKey: .oppositeCollapseKeywords)
|
||||||
|
|
||||||
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
try container.encode(showFavoriteAndReblogCounts, forKey: .showFavoriteAndReblogCounts)
|
||||||
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
try container.encode(defaultNotificationsMode, forKey: .defaultNotificationsType)
|
||||||
|
try container.encode(grayscaleImages, forKey: .grayscaleImages)
|
||||||
|
|
||||||
try container.encode(silentActions, forKey: .silentActions)
|
try container.encode(silentActions, forKey: .silentActions)
|
||||||
try container.encode(statusContentType, forKey: .statusContentType)
|
try container.encode(statusContentType, forKey: .statusContentType)
|
||||||
|
@ -124,16 +124,18 @@ class Preferences: Codable, ObservableObject {
|
||||||
@Published var inAppSafariAutomaticReaderMode = false
|
@Published var inAppSafariAutomaticReaderMode = false
|
||||||
@Published var expandAllContentWarnings = false
|
@Published var expandAllContentWarnings = false
|
||||||
@Published var collapseLongPosts = true
|
@Published var collapseLongPosts = true
|
||||||
|
@Published var oppositeCollapseKeywords: [String] = []
|
||||||
|
|
||||||
// MARK: Digital Wellness
|
// MARK: Digital Wellness
|
||||||
@Published var showFavoriteAndReblogCounts = true
|
@Published var showFavoriteAndReblogCounts = true
|
||||||
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
@Published var defaultNotificationsMode = NotificationsMode.allNotifications
|
||||||
|
@Published var grayscaleImages = false
|
||||||
|
|
||||||
// MARK: Advanced
|
// MARK: Advanced
|
||||||
@Published var silentActions: [String: Permission] = [:]
|
@Published var silentActions: [String: Permission] = [:]
|
||||||
@Published var statusContentType: StatusContentType = .plain
|
@Published var statusContentType: StatusContentType = .plain
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case theme
|
case theme
|
||||||
case avatarStyle
|
case avatarStyle
|
||||||
case hideCustomEmojiInUsernames
|
case hideCustomEmojiInUsernames
|
||||||
|
@ -154,9 +156,11 @@ class Preferences: Codable, ObservableObject {
|
||||||
case inAppSafariAutomaticReaderMode
|
case inAppSafariAutomaticReaderMode
|
||||||
case expandAllContentWarnings
|
case expandAllContentWarnings
|
||||||
case collapseLongPosts
|
case collapseLongPosts
|
||||||
|
case oppositeCollapseKeywords
|
||||||
|
|
||||||
case showFavoriteAndReblogCounts
|
case showFavoriteAndReblogCounts
|
||||||
case defaultNotificationsType
|
case defaultNotificationsType
|
||||||
|
case grayscaleImages
|
||||||
|
|
||||||
case silentActions
|
case silentActions
|
||||||
case statusContentType
|
case statusContentType
|
||||||
|
|
|
@ -11,10 +11,11 @@ import Gifu
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
protocol LargeImageContentView {
|
protocol LargeImageContentView: UIView {
|
||||||
var animationImage: UIImage? { get }
|
var animationImage: UIImage? { get }
|
||||||
var animationGifData: Data? { get }
|
var animationGifData: Data? { get }
|
||||||
var activityItemsForSharing: [Any] { get }
|
var activityItemsForSharing: [Any] { get }
|
||||||
|
func grayscaleStateChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
|
class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentView {
|
||||||
|
@ -29,6 +30,14 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
|
||||||
[image!]
|
[image!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var sourceData: Data?
|
||||||
|
|
||||||
|
convenience init(sourceData data: Data, isGif: Bool) {
|
||||||
|
self.init(image: UIImage(data: data)!, gifData: isGif ? data : nil)
|
||||||
|
|
||||||
|
self.sourceData = data
|
||||||
|
}
|
||||||
|
|
||||||
init(image: UIImage, gifData: Data?) {
|
init(image: UIImage, gifData: Data?) {
|
||||||
self.animationGifData = gifData
|
self.animationGifData = gifData
|
||||||
|
|
||||||
|
@ -50,6 +59,23 @@ class LargeImageImageContentView: UIImageView, GIFAnimatable, LargeImageContentV
|
||||||
|
|
||||||
updateImageIfNeeded()
|
updateImageIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func grayscaleStateChanged() {
|
||||||
|
guard let data = sourceData else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
image = ImageGrayscalifier.convert(url: nil, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
|
@ -85,4 +111,8 @@ class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func grayscaleStateChanged() {
|
||||||
|
// no-op, GifvAttachmentView observes the grayscale state itself
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,6 @@ import UIKit
|
||||||
|
|
||||||
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
|
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
|
||||||
|
|
||||||
typealias ContentView = UIView & LargeImageContentView
|
|
||||||
|
|
||||||
weak var animationSourceView: UIImageView?
|
weak var animationSourceView: UIImageView?
|
||||||
var largeImageController: LargeImageViewController? { self }
|
var largeImageController: LargeImageViewController? { self }
|
||||||
var animationImage: UIImage? { contentView.animationImage }
|
var animationImage: UIImage? { contentView.animationImage }
|
||||||
|
@ -31,7 +29,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
@IBOutlet weak var bottomControlsView: UIView!
|
@IBOutlet weak var bottomControlsView: UIView!
|
||||||
@IBOutlet weak var descriptionLabel: UILabel!
|
@IBOutlet weak var descriptionLabel: UILabel!
|
||||||
|
|
||||||
var contentView: ContentView {
|
var contentView: LargeImageContentView {
|
||||||
didSet {
|
didSet {
|
||||||
oldValue.removeFromSuperview()
|
oldValue.removeFromSuperview()
|
||||||
setupContentView()
|
setupContentView()
|
||||||
|
@ -50,7 +48,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
}
|
}
|
||||||
var shrinkGestureEnabled = true
|
var shrinkGestureEnabled = true
|
||||||
|
|
||||||
var prevZoomScale: CGFloat?
|
private var prevZoomScale: CGFloat?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
override var prefersStatusBarHidden: Bool {
|
||||||
return true
|
return true
|
||||||
|
@ -63,7 +62,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
return !controlsVisible
|
return !controlsVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
init(contentView: ContentView, description: String?, sourceView: UIImageView?) {
|
init(contentView: LargeImageContentView, description: String?, sourceView: UIImageView?) {
|
||||||
self.imageDescription = description
|
self.imageDescription = description
|
||||||
self.animationSourceView = sourceView
|
self.animationSourceView = sourceView
|
||||||
|
|
||||||
|
@ -103,6 +102,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
|
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
|
||||||
doubleTap.numberOfTapsRequired = 2
|
doubleTap.numberOfTapsRequired = 2
|
||||||
view.addGestureRecognizer(doubleTap)
|
view.addGestureRecognizer(doubleTap)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupContentView() {
|
private func setupContentView() {
|
||||||
|
@ -147,6 +148,13 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
closeButtonTrailingConstraint.constant = offset
|
closeButtonTrailingConstraint.constant = offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
contentView.grayscaleStateChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
||||||
self.controlsVisible = controlsVisible
|
self.controlsVisible = controlsVisible
|
||||||
|
|
|
@ -86,7 +86,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
|
|
||||||
if let data = cache.get(url) {
|
if let data = cache.get(url) {
|
||||||
createLargeImage(data: data)
|
createLargeImage(data: data, url: url)
|
||||||
} else {
|
} else {
|
||||||
createPreview()
|
createPreview()
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
self.imageRequest = nil
|
self.imageRequest = nil
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingVC?.removeViewAndController()
|
self.loadingVC?.removeViewAndController()
|
||||||
self.createLargeImage(data: data!)
|
self.createLargeImage(data: data!, url: self.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,12 +115,21 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createLargeImage(data: Data) {
|
private func createLargeImage(data: Data, url: URL) {
|
||||||
guard !loaded else { return }
|
guard !loaded else { return }
|
||||||
loaded = true
|
loaded = true
|
||||||
guard let image = UIImage(data: data) else { return }
|
|
||||||
let gifData = url.pathExtension == "gif" ? data : nil
|
let image: UIImage?
|
||||||
createLargeImage(image: image, gifData: gifData)
|
if Preferences.shared.grayscaleImages {
|
||||||
|
image = ImageGrayscalifier.convert(url: url, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
let gifData = url.pathExtension == "gif" ? data : nil
|
||||||
|
createLargeImage(image: image, gifData: gifData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createLargeImage(image: UIImage, gifData: Data?) {
|
private func createLargeImage(image: UIImage, gifData: Data?) {
|
||||||
|
@ -138,8 +147,13 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
|
||||||
|
|
||||||
private func createPreview() {
|
private func createPreview() {
|
||||||
guard !self.loaded,
|
guard !self.loaded,
|
||||||
let image = animationSourceView?.image else { return }
|
var image = animationSourceView?.image else { return }
|
||||||
|
|
||||||
|
if Preferences.shared.grayscaleImages,
|
||||||
|
let source = image.cgImage,
|
||||||
|
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
||||||
|
image = grayscale
|
||||||
|
}
|
||||||
self.createLargeImage(image: image, gifData: nil)
|
self.createLargeImage(image: image, gifData: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,12 +36,16 @@ struct BehaviorPrefsView: View {
|
||||||
|
|
||||||
var contentWarningsSection: some View {
|
var contentWarningsSection: some View {
|
||||||
Section(header: Text("Content Warnings")) {
|
Section(header: Text("Content Warnings")) {
|
||||||
|
Toggle(isOn: $preferences.collapseLongPosts) {
|
||||||
|
Text("Collapse Long Posts")
|
||||||
|
}
|
||||||
|
|
||||||
Toggle(isOn: $preferences.expandAllContentWarnings) {
|
Toggle(isOn: $preferences.expandAllContentWarnings) {
|
||||||
Text("Expand All Content Warnings")
|
Text("Expand All Content Warnings")
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $preferences.collapseLongPosts) {
|
NavigationLink(destination: OppositeCollapseKeywordsView()) {
|
||||||
Text("Collapse Long Posts")
|
Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
//
|
||||||
|
// OppositeCollapseKeywordsView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/1/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct OppositeCollapseKeywordsView: View {
|
||||||
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
|
// Can't use the raw [String] for keywords, because we need a fixed ID (within the lifetime of this view) for ForEach
|
||||||
|
@State private var keywords: [Keyword] = Preferences.shared.oppositeCollapseKeywords.map(Keyword.init) {
|
||||||
|
didSet {
|
||||||
|
preferences.oppositeCollapseKeywords = keywords.map(\.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@State private var valueToAdd = ""
|
||||||
|
@State private var makeAddFieldFirstResponder = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing
|
||||||
|
// the color behind it can be seen, which looks odd
|
||||||
|
Color(UIColor.secondarySystemBackground)
|
||||||
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
|
|
||||||
|
List {
|
||||||
|
Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) {
|
||||||
|
ForEach(keywords) { (keyword) in
|
||||||
|
Row(keyword: keyword) {
|
||||||
|
keywords.removeAll(where: { $0.id == keyword.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: self.removeKeywords)
|
||||||
|
|
||||||
|
FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animation(.default)
|
||||||
|
.listStyle(GroupedListStyle())
|
||||||
|
}
|
||||||
|
.onAppear(perform: updateAppearance)
|
||||||
|
.navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAppearance() {
|
||||||
|
UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commitExisting(at index: Int) -> () -> Void {
|
||||||
|
return {
|
||||||
|
if keywords[index].value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
keywords.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeAddFieldFirstResponder = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeKeywords(_ indices: IndexSet) {
|
||||||
|
keywords.remove(atOffsets: indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addKeyword() {
|
||||||
|
guard !valueToAdd.isEmpty else { return }
|
||||||
|
keywords.append(Keyword(valueToAdd))
|
||||||
|
valueToAdd = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension OppositeCollapseKeywordsView {
|
||||||
|
// Class for wrapping keywords that provides a fixed id SwiftUI's ForEach can use
|
||||||
|
class Keyword: ObservableObject, Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
@Published var value: String
|
||||||
|
|
||||||
|
init(_ value: String) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension OppositeCollapseKeywordsView {
|
||||||
|
// Use a separate View for the row so it can use @ObservableObject to get a binding for the keyword's value
|
||||||
|
struct Row: View {
|
||||||
|
@ObservedObject var keyword: Keyword
|
||||||
|
let removeKeyword: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
FocusableTextField(placeholder: "Keyword", text: $keyword.value) {
|
||||||
|
if keyword.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
removeKeyword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OppositeCollapseKeywordsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
OppositeCollapseKeywordsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,31 +9,28 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct WellnessPrefsView: View {
|
struct WellnessPrefsView: View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject private var preferences = Preferences.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
showFavAndReblogCountSection
|
showFavAndReblogCount
|
||||||
notificationsModeSection
|
notificationsMode
|
||||||
|
grayscaleImages
|
||||||
}
|
}
|
||||||
.insetOrGroupedListStyle()
|
.insetOrGroupedListStyle()
|
||||||
.navigationBarTitle(Text("Digital Wellness"))
|
.navigationBarTitle(Text("Digital Wellness"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var showFavAndReblogCountSection: some View {
|
private var showFavAndReblogCount: some View {
|
||||||
Section(footer: showFavAndReblogCountFooter) {
|
Section(footer: Text("Control whether total favorite and reblog counts are shown for the main post in conversations.")) {
|
||||||
Toggle(isOn: $preferences.showFavoriteAndReblogCounts) {
|
Toggle(isOn: $preferences.showFavoriteAndReblogCounts) {
|
||||||
Text("Show Favorite and Reblog Counts")
|
Text("Show Favorite and Reblog Counts")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var showFavAndReblogCountFooter: some View {
|
private var notificationsMode: some View {
|
||||||
Text("Control whether total favorite and reblog counts are shown for the main post in conversations.")
|
Section(footer: Text("Choose which kinds of notifications will be shown by default in the Notifications tab.")) {
|
||||||
}
|
|
||||||
|
|
||||||
var notificationsModeSection: some View {
|
|
||||||
Section(footer: notificationsModeFooter) {
|
|
||||||
Picker(selection: $preferences.defaultNotificationsMode, label: Text("Default Notifications Mode")) {
|
Picker(selection: $preferences.defaultNotificationsMode, label: Text("Default Notifications Mode")) {
|
||||||
ForEach(NotificationsMode.allCases, id: \.self) { type in
|
ForEach(NotificationsMode.allCases, id: \.self) { type in
|
||||||
Text(type.displayName).tag(type)
|
Text(type.displayName).tag(type)
|
||||||
|
@ -42,8 +39,12 @@ struct WellnessPrefsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationsModeFooter: some View {
|
private var grayscaleImages: some View {
|
||||||
Text("Choose which kinds of notifications will be shown by default in the Notifications tab.")
|
Section(footer: Text("Show attachments, avatars, headers, and custom emoji in black and white.")) {
|
||||||
|
Toggle(isOn: $preferences.grayscaleImages) {
|
||||||
|
Text("Grayscale Images")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,21 @@ class MyProfileViewController: ProfileViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
|
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
|
||||||
_ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in
|
let avatarURL = account.avatar
|
||||||
guard let self = self, let data = data, let image = UIImage(data: data) else { return }
|
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data else { return }
|
||||||
|
|
||||||
|
let maybeGrayscale: UIImage?
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
maybeGrayscale = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
maybeGrayscale = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let image = maybeGrayscale else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let size = CGSize(width: 30, height: 30)
|
let size = CGSize(width: 30, height: 30)
|
||||||
let rect = CGRect(origin: .zero, size: size)
|
let rect = CGRect(origin: .zero, size: size)
|
||||||
|
|
|
@ -21,7 +21,8 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var accountID: String!
|
var accountID: String!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
private var avatarRequest: ImageCache.Request?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
@ -38,6 +39,10 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
fatalError("Missing cached account \(accountID!)")
|
fatalError("Missing cached account \(accountID!)")
|
||||||
}
|
}
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
updateGrayscaleableUI(account: account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(accountID: String) {
|
func updateUI(accountID: String) {
|
||||||
|
@ -46,21 +51,37 @@ class AccountTableViewCell: UITableViewCell {
|
||||||
fatalError("Missing cached account \(accountID)")
|
fatalError("Missing cached account \(accountID)")
|
||||||
}
|
}
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
|
updateGrayscaleableUI(account: account)
|
||||||
|
updateUIForPrefrences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateGrayscaleableUI(account: AccountMO) {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let accountID = self.accountID
|
||||||
|
|
||||||
|
let avatarURL = account.avatar
|
||||||
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||||
self.avatarRequest = nil
|
self.avatarRequest = nil
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
self.avatarImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameLabel.text = "@\(account.acct)"
|
|
||||||
|
|
||||||
let doc = try! SwiftSoup.parse(account.note)
|
let doc = try! SwiftSoup.parse(account.note)
|
||||||
noteLabel.text = try! doc.text()
|
noteLabel.text = try! doc.text()
|
||||||
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
|
||||||
updateUIForPrefrences()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
|
|
|
@ -28,11 +28,21 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
var expectedSize: CGSize!
|
var expectedSize: CGSize!
|
||||||
|
|
||||||
private var attachmentRequest: ImageCache.Request?
|
private var attachmentRequest: ImageCache.Request?
|
||||||
|
private var source: Source?
|
||||||
|
|
||||||
var gifData: Data?
|
var gifData: Data? {
|
||||||
|
switch source {
|
||||||
|
case let .gifData(_, data):
|
||||||
|
return data
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
private var autoplayGifs: Bool {
|
private var autoplayGifs: Bool {
|
||||||
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
public lazy var animator: Animator? = Animator(withDelegate: self)
|
public lazy var animator: Animator? = Animator(withDelegate: self)
|
||||||
|
|
||||||
|
@ -55,19 +65,29 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
commonInit()
|
commonInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func commonInit() {
|
private func commonInit() {
|
||||||
contentMode = .scaleAspectFill
|
contentMode = .scaleAspectFill
|
||||||
layer.masksToBounds = true
|
layer.masksToBounds = true
|
||||||
isUserInteractionEnabled = true
|
isUserInteractionEnabled = true
|
||||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
|
||||||
|
|
||||||
addInteraction(UIContextMenuInteraction(delegate: self))
|
addInteraction(UIContextMenuInteraction(delegate: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func gifPlaybackModeChanged() {
|
@objc private func preferencesChanged() {
|
||||||
|
gifPlaybackModeChanged()
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
ImageGrayscalifier.queue.async {
|
||||||
|
self.displayImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func gifPlaybackModeChanged() {
|
||||||
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
// NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.attachment.kind == .image,
|
if self.attachment.kind == .image,
|
||||||
|
@ -106,13 +126,19 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
} else {
|
} else {
|
||||||
size = self.expectedSize
|
size = self.expectedSize
|
||||||
}
|
}
|
||||||
if let preview = UIImage(blurHash: hash, size: size) {
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
guard var preview = UIImage(blurHash: hash, size: size) else {
|
||||||
guard let self = self else { return }
|
return
|
||||||
if self.image == nil {
|
}
|
||||||
self.image = preview
|
|
||||||
}
|
if Preferences.shared.grayscaleImages,
|
||||||
}
|
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
|
||||||
|
preview = grayscale
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self, self.image == nil else { return }
|
||||||
|
self.image = preview
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,35 +158,36 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadImage() {
|
func loadImage() {
|
||||||
attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in
|
let attachmentURL = attachment.url
|
||||||
|
attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data) in
|
||||||
guard let self = self, let data = data else { return }
|
guard let self = self, let data = data else { return }
|
||||||
self.attachmentRequest = nil
|
self.attachmentRequest = nil
|
||||||
DispatchQueue.main.async {
|
if self.attachment.url.pathExtension == "gif" {
|
||||||
if self.attachment.url.pathExtension == "gif" {
|
self.source = .gifData(attachmentURL, data)
|
||||||
self.gifData = data
|
if self.autoplayGifs {
|
||||||
if self.autoplayGifs {
|
DispatchQueue.main.async {
|
||||||
self.animate(withGIFData: data)
|
self.animate(withGIFData: data)
|
||||||
} else {
|
|
||||||
self.image = UIImage(data: data)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.image = UIImage(data: data)
|
self.displayImage()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.source = .imageData(attachmentURL, data)
|
||||||
|
self.displayImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVideo() {
|
func loadVideo() {
|
||||||
let attachmentURL = self.attachment.url
|
let attachmentURL = self.attachment.url
|
||||||
|
// todo: use a single dispatch queue
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
let asset = AVURLAsset(url: attachmentURL)
|
let asset = AVURLAsset(url: attachmentURL)
|
||||||
let generator = AVAssetImageGenerator(asset: asset)
|
let generator = AVAssetImageGenerator(asset: asset)
|
||||||
generator.appliesPreferredTrackTransform = true
|
generator.appliesPreferredTrackTransform = true
|
||||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||||
DispatchQueue.main.async { [weak self] in
|
self.source = .cgImage(attachmentURL, image)
|
||||||
guard let self = self, self.attachment.url == attachmentURL else { return }
|
self.displayImage()
|
||||||
self.image = UIImage(cgImage: image)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
|
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
|
||||||
|
@ -202,10 +229,8 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
let generator = AVAssetImageGenerator(asset: asset)
|
let generator = AVAssetImageGenerator(asset: asset)
|
||||||
generator.appliesPreferredTrackTransform = true
|
generator.appliesPreferredTrackTransform = true
|
||||||
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
|
||||||
DispatchQueue.main.async { [weak self] in
|
self.source = .cgImage(attachmentURL, image)
|
||||||
guard let self = self, self.attachment.url == attachmentURL else { return }
|
self.displayImage()
|
||||||
self.image = UIImage(cgImage: image)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
||||||
|
@ -223,6 +248,35 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func displayImage() {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
|
||||||
|
switch source {
|
||||||
|
case nil:
|
||||||
|
image = nil
|
||||||
|
|
||||||
|
case let .imageData(url, data), let .gifData(url, data):
|
||||||
|
if isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: url, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .cgImage(url, cgImage):
|
||||||
|
if isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
|
||||||
|
} else {
|
||||||
|
image = UIImage(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func display(_ layer: CALayer) {
|
override func display(_ layer: CALayer) {
|
||||||
super.display(layer)
|
super.display(layer)
|
||||||
|
|
||||||
|
@ -242,6 +296,14 @@ class AttachmentView: UIImageView, GIFAnimatable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate extension AttachmentView {
|
||||||
|
enum Source {
|
||||||
|
case imageData(URL, Data)
|
||||||
|
case gifData(URL, Data)
|
||||||
|
case cgImage(URL, CGImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension AttachmentView: UIContextMenuInteractionDelegate {
|
extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
|
||||||
|
|
|
@ -19,12 +19,16 @@ class GifvAttachmentView: UIView {
|
||||||
layer as! AVPlayerLayer
|
layer as! AVPlayerLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
let item: AVPlayerItem
|
private var asset: AVAsset
|
||||||
|
private(set) var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
|
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
|
||||||
item = AVPlayerItem(asset: asset)
|
self.asset = asset
|
||||||
|
item = GifvAttachmentView.createItem(asset: asset)
|
||||||
player = AVPlayer(playerItem: item)
|
player = AVPlayer(playerItem: item)
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -33,13 +37,39 @@ class GifvAttachmentView: UIView {
|
||||||
player.isMuted = true
|
player.isMuted = true
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func restartItem() {
|
private static func createItem(asset: AVAsset) -> AVPlayerItem {
|
||||||
|
let item = AVPlayerItem(asset: asset)
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { (request) in
|
||||||
|
let filter = CIFilter(name: "CIColorMonochrome")!
|
||||||
|
|
||||||
|
filter.setValue(request.sourceImage, forKey: "inputImage")
|
||||||
|
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
||||||
|
filter.setValue(1.0, forKey: "inputIntensity")
|
||||||
|
|
||||||
|
request.finish(with: filter.outputImage!, context: nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
item = GifvAttachmentView.createItem(asset: asset)
|
||||||
|
player.replaceCurrentItem(with: item)
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func restartItem() {
|
||||||
item.seek(to: .zero) { (success) in
|
item.seek(to: .zero) { (success) in
|
||||||
guard success else { return }
|
guard success else { return }
|
||||||
self.player.play()
|
self.player.play()
|
||||||
|
|
|
@ -45,10 +45,18 @@ extension BaseEmojiLabel {
|
||||||
group.enter()
|
group.enter()
|
||||||
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
let request = ImageCache.emojis.get(emoji.url) { (data) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let data = data, let image = UIImage(data: data) else {
|
guard let data = data else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emojiImages[emoji.shortcode] = image
|
let image: UIImage?
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
if let image = image {
|
||||||
|
emojiImages[emoji.shortcode] = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let request = request {
|
if let request = request {
|
||||||
emojiRequests.append(request)
|
emojiRequests.append(request)
|
||||||
|
|
|
@ -59,10 +59,18 @@ class ContentTextView: LinkTextView {
|
||||||
group.enter()
|
group.enter()
|
||||||
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
_ = ImageCache.emojis.get(emoji.url) { (data) in
|
||||||
defer { group.leave() }
|
defer { group.leave() }
|
||||||
guard let data = data, let image = UIImage(data: data) else {
|
guard let data = data else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
emojiImages[emoji.shortcode] = image
|
let image: UIImage?
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
image = ImageGrayscalifier.convert(url: emoji.url, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
if let image = image {
|
||||||
|
emojiImages[emoji.shortcode] = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
//
|
||||||
|
// FocusableTextField.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/2/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FocusableTextField: UIViewRepresentable {
|
||||||
|
typealias UIViewType = UITextField
|
||||||
|
|
||||||
|
let placeholder: String
|
||||||
|
let text: Binding<String>
|
||||||
|
let becomeFirstResponder: Binding<Bool>?
|
||||||
|
let onCommit: (() -> Void)?
|
||||||
|
|
||||||
|
init(placeholder: String, text: Binding<String>, becomeFirstResponder: Binding<Bool>? = nil, onCommit: (() -> Void)? = nil) {
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self.text = text
|
||||||
|
self.becomeFirstResponder = becomeFirstResponder
|
||||||
|
self.onCommit = onCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UITextField {
|
||||||
|
let field = UITextField()
|
||||||
|
field.delegate = context.coordinator
|
||||||
|
field.addTarget(context.coordinator, action: #selector(Coordinator.didChange(_:)), for: .editingChanged)
|
||||||
|
field.addTarget(context.coordinator, action: #selector(Coordinator.didEnd(_:)), for: .primaryActionTriggered)
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UITextField, context: Context) {
|
||||||
|
uiView.placeholder = placeholder
|
||||||
|
uiView.text = text.wrappedValue
|
||||||
|
|
||||||
|
context.coordinator.text = text
|
||||||
|
context.coordinator.onCommit = onCommit
|
||||||
|
|
||||||
|
if becomeFirstResponder?.wrappedValue == true {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
uiView.becomeFirstResponder()
|
||||||
|
becomeFirstResponder?.wrappedValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
return Coordinator(text: text)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UITextFieldDelegate {
|
||||||
|
var text: Binding<String>
|
||||||
|
var onCommit: (() -> Void)?
|
||||||
|
|
||||||
|
init(text: Binding<String>) {
|
||||||
|
self.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didChange(_ textField: UITextField) {
|
||||||
|
text.wrappedValue = textField.text ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didEnd(_ textField: UITextField) {
|
||||||
|
onCommit?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
onCommit?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,8 +25,9 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
var group: NotificationGroup!
|
var group: NotificationGroup!
|
||||||
var statusID: String!
|
var statusID: String!
|
||||||
|
|
||||||
var avatarRequests = [String: ImageCache.Request]()
|
private var avatarRequests = [String: ImageCache.Request]()
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
private var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
|
@ -44,6 +45,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
|
for case let imageView as UIImageView in actionAvatarStackView.arrangedSubviews {
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
updateGrayscaleableUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(group: NotificationGroup) {
|
func updateUI(group: NotificationGroup) {
|
||||||
|
@ -67,8 +72,10 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||||
|
|
||||||
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
actionAvatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
var imageViews = [UIImageView]()
|
var imageViews = [UIImageView]()
|
||||||
for account in people {
|
for account in people {
|
||||||
|
@ -76,11 +83,22 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
let avatarURL = account.avatar
|
||||||
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let self = self, let data = data, self.group.id == group.id else { return }
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
let image: UIImage?
|
||||||
imageView.image = UIImage(data: data)
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequests.removeValue(forKey: account.id)
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actionAvatarStackView.addArrangedSubview(imageView)
|
actionAvatarStackView.addArrangedSubview(imageView)
|
||||||
|
@ -104,7 +122,38 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
|
||||||
statusContentLabel.text = try! doc.text()
|
statusContentLabel.text = try! doc.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimestamp() {
|
private func updateGrayscaleableUI() {
|
||||||
|
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||||
|
let groupID = group.id
|
||||||
|
|
||||||
|
for (index, account) in people.enumerated() {
|
||||||
|
guard actionAvatarStackView.arrangedSubviews.count > index,
|
||||||
|
let imageView = actionAvatarStackView.arrangedSubviews[index] as? UIImageView else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarURL = account.avatar
|
||||||
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data, self.group.id == groupID else { return }
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequests.removeValue(forKey: account.id)
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTimestamp() {
|
||||||
guard let notification = group.notifications.first else {
|
guard let notification = group.notifications.first else {
|
||||||
fatalError("Missing cached notification")
|
fatalError("Missing cached notification")
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,9 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
var group: NotificationGroup!
|
var group: NotificationGroup!
|
||||||
|
|
||||||
var avatarRequests = [String: ImageCache.Request]()
|
private var avatarRequests = [String: ImageCache.Request]()
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
private var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
|
@ -39,6 +40,10 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
|
for case let imageView as UIImageView in avatarStackView.arrangedSubviews {
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
updateGrayscaleableUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(group: NotificationGroup) {
|
func updateUI(group: NotificationGroup) {
|
||||||
|
@ -51,17 +56,30 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
}, identifier: group.id)
|
}, identifier: group.id)
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
|
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
avatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
avatarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
for account in people {
|
for account in people {
|
||||||
let imageView = UIImageView()
|
let imageView = UIImageView()
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.layer.masksToBounds = true
|
imageView.layer.masksToBounds = true
|
||||||
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
imageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
let avatarURL = account.avatar
|
||||||
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
guard let self = self, let data = data, self.group.id == group.id else { return }
|
guard let self = self, let data = data, self.group.id == group.id else { return }
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.avatarRequests.removeValue(forKey: account.id)
|
let image: UIImage?
|
||||||
imageView.image = UIImage(data: data)
|
if Preferences.shared.grayscaleImages {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequests.removeValue(forKey: account.id)
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
avatarStackView.addArrangedSubview(imageView)
|
avatarStackView.addArrangedSubview(imageView)
|
||||||
|
@ -72,6 +90,39 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateGrayscaleableUI() {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let people = group.notifications.compactMap { mastodonController.persistentContainer.account(for: $0.account.id) }
|
||||||
|
let groupID = group.id
|
||||||
|
|
||||||
|
for (index, account) in people.enumerated() {
|
||||||
|
guard avatarStackView.arrangedSubviews.count > index,
|
||||||
|
let imageView = avatarStackView.arrangedSubviews[index] as? UIImageView else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarURL = account.avatar
|
||||||
|
avatarRequests[account.id] = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data, self.group.id == groupID else { return }
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarRequests.removeValue(forKey: account.id)
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString {
|
func updateActionLabel(names: [NSAttributedString]) -> NSAttributedString {
|
||||||
// todo: figure out how to localize this
|
// todo: figure out how to localize this
|
||||||
let str = NSMutableAttributedString(string: "Followed by ")
|
let str = NSMutableAttributedString(string: "Followed by ")
|
||||||
|
|
|
@ -25,8 +25,9 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
var notification: Pachyderm.Notification?
|
var notification: Pachyderm.Notification?
|
||||||
var account: Account!
|
var account: Account!
|
||||||
|
|
||||||
var avatarRequest: ImageCache.Request?
|
private var avatarRequest: ImageCache.Request?
|
||||||
var updateTimestampWorkItem: DispatchWorkItem?
|
private var updateTimestampWorkItem: DispatchWorkItem?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
updateTimestampWorkItem?.cancel()
|
updateTimestampWorkItem?.cancel()
|
||||||
|
@ -43,6 +44,11 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
@objc func updateUIForPreferences() {
|
@objc func updateUIForPreferences() {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 30
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages,
|
||||||
|
let account = self.account {
|
||||||
|
updateUI(account: account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(notification: Pachyderm.Notification) {
|
func updateUI(notification: Pachyderm.Notification) {
|
||||||
|
@ -61,16 +67,27 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
|
||||||
actionLabel.text = "Request to follow from \(account.displayName)"
|
actionLabel.text = "Request to follow from \(account.displayName)"
|
||||||
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
actionLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
}
|
}
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
let avatarURL = account.avatar
|
||||||
guard let self = self, self.account == account, let data = data, let image = UIImage(data: data) else { return }
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
|
guard let self = self, self.account == account, let data = data else { return }
|
||||||
self.avatarRequest = nil
|
self.avatarRequest = nil
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.avatarImageView.image = image
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarImageView.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTimestamp() {
|
private func updateTimestamp() {
|
||||||
guard let notification = notification else { return }
|
guard let notification = notification else { return }
|
||||||
|
|
||||||
timestampLabel.text = notification.createdAt.timeAgoString()
|
timestampLabel.text = notification.createdAt.timeAgoString()
|
||||||
|
|
|
@ -48,6 +48,8 @@ class ProfileHeaderView: UIView {
|
||||||
private var avatarRequest: ImageCache.Request?
|
private var avatarRequest: ImageCache.Request?
|
||||||
private var headerRequest: ImageCache.Request?
|
private var headerRequest: ImageCache.Request?
|
||||||
|
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
@ -106,22 +108,7 @@ class ProfileHeaderView: UIView {
|
||||||
|
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
|
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
updateImages(account: account)
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
|
||||||
self.avatarRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let header = account.header {
|
|
||||||
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
|
|
||||||
guard let self = self, let data = data, self.accountID == accountID else { return }
|
|
||||||
self.headerRequest = nil
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.headerImageView.image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if #available(iOS 14.0, *) {
|
if #available(iOS 14.0, *) {
|
||||||
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: actionsForProfile(accountID: accountID, sourceView: moreButton))
|
||||||
|
@ -193,6 +180,49 @@ class ProfileHeaderView: UIView {
|
||||||
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)
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
updateImages(account: account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateImages(account: AccountMO) {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let accountID = account.id
|
||||||
|
let avatarURL = account.avatar
|
||||||
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||||
|
self.avatarRequest = nil
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarImageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let header = account.header {
|
||||||
|
headerRequest = ImageCache.headers.get(header) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||||
|
self.headerRequest = nil
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: header, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.headerImageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Interaction
|
// MARK: Interaction
|
||||||
|
|
|
@ -74,6 +74,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
private var currentPictureInPictureVideoStatusID: String?
|
private var currentPictureInPictureVideoStatusID: String?
|
||||||
|
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
@ -96,6 +98,8 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
contentWarningLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
open func createObserversIfNecessary() {
|
open func createObserversIfNecessary() {
|
||||||
|
@ -142,6 +146,7 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
let account = status.account
|
let account = status.account
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
updateUI(account: account)
|
updateUI(account: account)
|
||||||
|
updateGrayscaleableUI(account: account, status: status)
|
||||||
updateUIForPreferences(account: account, status: status)
|
updateUIForPreferences(account: account, status: status)
|
||||||
|
|
||||||
cardView.card = status.card
|
cardView.card = status.card
|
||||||
|
@ -154,8 +159,6 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
updateStatusState(status: status)
|
updateStatusState(status: status)
|
||||||
|
|
||||||
contentTextView.setTextFrom(status: status)
|
|
||||||
|
|
||||||
contentWarningLabel.text = status.spoilerText
|
contentWarningLabel.text = status.spoilerText
|
||||||
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
contentWarningLabel.isHidden = status.spoilerText.isEmpty
|
||||||
if !contentWarningLabel.isHidden {
|
if !contentWarningLabel.isHidden {
|
||||||
|
@ -215,12 +218,6 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
func updateUI(account: AccountMO) {
|
func updateUI(account: AccountMO) {
|
||||||
usernameLabel.text = "@\(account.acct)"
|
usernameLabel.text = "@\(account.acct)"
|
||||||
avatarImageView.image = nil
|
avatarImageView.image = nil
|
||||||
avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let self = self, let data = data, self.accountID == account.id else { return }
|
|
||||||
self.avatarImageView.image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
|
@ -232,10 +229,13 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
|
||||||
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
attachmentsView.contentHidden = Preferences.shared.blurAllMedia || (mastodonController.persistentContainer.status(for: statusID)?.sensitive ?? false)
|
||||||
|
|
||||||
updateStatusIconsForPreferences(status)
|
updateStatusIconsForPreferences(status)
|
||||||
|
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
updateGrayscaleableUI(account: account, status: status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStatusIconsForPreferences(_ status: StatusMO) {
|
func updateStatusIconsForPreferences(_ status: StatusMO) {
|
||||||
|
@ -253,6 +253,31 @@ class BaseStatusTableViewCell: UITableViewCell {
|
||||||
reblogButton.setImage(reblogButtonImage, for: .normal)
|
reblogButton.setImage(reblogButtonImage, for: .normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
let avatarURL = account.avatar
|
||||||
|
let accountID = account.id
|
||||||
|
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in
|
||||||
|
guard let self = self, let data = data, self.accountID == accountID else { return }
|
||||||
|
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.avatarImageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTextView.setTextFrom(status: status)
|
||||||
|
|
||||||
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
}
|
||||||
|
|
||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,8 @@
|
||||||
<constraint firstItem="3Qu-IO-5wt" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="pPU-WS-Y6B"/>
|
<constraint firstItem="3Qu-IO-5wt" firstAttribute="top" secondItem="Cnd-Fj-B7l" secondAttribute="top" id="pPU-WS-Y6B"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cwQ-mR-L1b" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="58" width="137.5" height="20.5"/>
|
<rect key="frame" x="0.0" y="58" width="343" height="20.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||||
</accessibility>
|
</accessibility>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh">
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="86.5" width="343" height="30"/>
|
<rect key="frame" x="0.0" y="86.5" width="343" height="30"/>
|
||||||
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
@ -216,6 +216,7 @@
|
||||||
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
|
<constraint firstItem="Cnd-Fj-B7l" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="2hS-RG-81T"/>
|
||||||
<constraint firstItem="z0g-HN-gS0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="4TF-2Z-mdf"/>
|
<constraint firstItem="z0g-HN-gS0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="4TF-2Z-mdf"/>
|
||||||
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
|
<constraint firstItem="IF9-9U-Gk0" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="8A8-wi-7sg"/>
|
||||||
|
<constraint firstItem="cwQ-mR-L1b" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="O32-3Q-mUs"/>
|
||||||
<constraint firstItem="8r8-O8-Agh" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="bZv-bR-jJ3"/>
|
<constraint firstItem="8r8-O8-Agh" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="bZv-bR-jJ3"/>
|
||||||
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
<constraint firstItem="ejU-sO-Og5" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="biK-oQ-SLy"/>
|
||||||
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
<constraint firstItem="3Bg-XP-d13" firstAttribute="width" secondItem="GuG-Qd-B8I" secondAttribute="width" id="iIq-gh-90O"/>
|
||||||
|
|
|
@ -26,6 +26,7 @@ class StatusCardView: UIView {
|
||||||
private let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
private let inactiveBackgroundColor = UIColor.secondarySystemBackground
|
||||||
|
|
||||||
private var imageRequest: ImageCache.Request?
|
private var imageRequest: ImageCache.Request?
|
||||||
|
private var isGrayscale = false
|
||||||
|
|
||||||
private var titleLabel: UILabel!
|
private var titleLabel: UILabel!
|
||||||
private var descriptionLabel: UILabel!
|
private var descriptionLabel: UILabel!
|
||||||
|
@ -108,26 +109,15 @@ class StatusCardView: UIView {
|
||||||
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
placeholderImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
|
||||||
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
placeholderImageView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateUI(card: Card) {
|
private func updateUI(card: Card) {
|
||||||
self.imageView.image = nil
|
self.imageView.image = nil
|
||||||
|
|
||||||
if let image = card.image {
|
updateGrayscaleableUI(card: card)
|
||||||
placeholderImageView.isHidden = true
|
updateUIForPreferences()
|
||||||
|
|
||||||
imageRequest = ImageCache.attachments.get(image, completion: { (data) in
|
|
||||||
guard let data = data, let image = UIImage(data: data) else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.imageView.image = image
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if imageRequest != nil {
|
|
||||||
loadBlurHash()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
placeholderImageView.isHidden = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
|
@ -138,6 +128,41 @@ class StatusCardView: UIView {
|
||||||
descriptionLabel.isHidden = description.isEmpty
|
descriptionLabel.isHidden = description.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func updateUIForPreferences() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages,
|
||||||
|
let card = card {
|
||||||
|
updateGrayscaleableUI(card: card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateGrayscaleableUI(card: Card) {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
|
if let imageURL = card.image {
|
||||||
|
placeholderImageView.isHidden = true
|
||||||
|
|
||||||
|
imageRequest = ImageCache.attachments.get(imageURL, completion: { (data) in
|
||||||
|
guard let data = data else { return }
|
||||||
|
let image: UIImage?
|
||||||
|
if self.isGrayscale {
|
||||||
|
image = ImageGrayscalifier.convert(url: imageURL, data: data)
|
||||||
|
} else {
|
||||||
|
image = UIImage(data: data)
|
||||||
|
}
|
||||||
|
if let image = image {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.imageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if imageRequest != nil {
|
||||||
|
loadBlurHash()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
placeholderImageView.isHidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadBlurHash() {
|
private func loadBlurHash() {
|
||||||
guard let card = card, let hash = card.blurhash else { return }
|
guard let card = card, let hash = card.blurhash else { return }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// StatusCollapseButton.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/3/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class StatusCollapseButton: UIButton {
|
||||||
|
|
||||||
|
private var interactionBounds: CGRect!
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
interactionBounds = bounds.inset(by: UIEdgeInsets(top: -8, left: 0, bottom: 0, right: 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||||
|
return interactionBounds.contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -94,8 +94,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
pinImageView.isHidden = !pinned
|
pinImageView.isHidden = !pinned
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
|
override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) {
|
||||||
super.updateUIForPreferences(account: account, status: status)
|
super.updateGrayscaleableUI(account: account, status: status)
|
||||||
|
|
||||||
if let rebloggerID = rebloggerID,
|
if let rebloggerID = rebloggerID,
|
||||||
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
|
||||||
|
|
|
@ -84,16 +84,17 @@
|
||||||
<constraint firstAttribute="height" secondItem="gll-xe-FSr" secondAttribute="height" id="B7p-Pc-fZD"/>
|
<constraint firstAttribute="height" secondItem="gll-xe-FSr" secondAttribute="height" id="B7p-Pc-fZD"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</stackView>
|
</stackView>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="755" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="inI-Og-YiU" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="755" text="Content Warning" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="inI-Og-YiU" customClass="EmojiLabel" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="24.5" width="277" height="20.5"/>
|
<rect key="frame" x="0.0" y="24.5" width="277" height="20.5"/>
|
||||||
<accessibility key="accessibilityConfiguration">
|
<accessibility key="accessibilityConfiguration">
|
||||||
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
|
||||||
</accessibility>
|
</accessibility>
|
||||||
|
<gestureRecognizers/>
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||||
<nil key="highlightedColor"/>
|
<nil key="highlightedColor"/>
|
||||||
</label>
|
</label>
|
||||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="O0E-Vf-XYR">
|
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="O0E-Vf-XYR" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="49" width="277" height="30"/>
|
<rect key="frame" x="0.0" y="49" width="277" height="30"/>
|
||||||
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
<color key="backgroundColor" systemColor="systemBlueColor"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
|
Loading…
Reference in New Issue