Compare commits

..

No commits in common. "348c306858990a6690175d66d16f030c92b4ef28" and "89b35fab6dca1b7dd84fe2346b3f10bc16451e1a" have entirely different histories.

28 changed files with 146 additions and 842 deletions

View File

@ -76,7 +76,6 @@ 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
} }
@ -91,7 +90,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.isEmpty ? nil : request.queryParameters.queryItems components.queryItems = 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

View File

@ -231,8 +231,6 @@
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 */; };
@ -282,10 +280,8 @@
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 */; };
@ -573,8 +569,6 @@
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; };
@ -630,10 +624,8 @@
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>"; };
@ -1055,7 +1047,6 @@
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 */,
@ -1075,7 +1066,6 @@
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>";
@ -1311,7 +1301,6 @@
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 */,
@ -1396,7 +1385,6 @@
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 */,
@ -1854,7 +1842,6 @@
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 */,
@ -1879,7 +1866,6 @@
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 */,
@ -1888,10 +1874,8 @@
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 */,

View File

@ -23,23 +23,8 @@ 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
// use ?? instead of || because the content warnig pref takes priority over length self.collapsed = longEnoughToCollapse || (!Preferences.shared.expandAllContentWarnings && contentWarningCollapsible)
self.collapsed = collapseDueToContentWarning ?? longEnoughToCollapse
} }
} }

View File

@ -1,59 +0,0 @@
//
// 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
}
}

View File

@ -55,13 +55,15 @@ 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)
self.expandAllContentWarnings = try container.decodeIfPresent(Bool.self, forKey: .expandAllContentWarnings) ?? false if container.contains(.expandAllContentWarnings) {
self.collapseLongPosts = try container.decodeIfPresent(Bool.self, forKey: .collapseLongPosts) ?? true self.expandAllContentWarnings = try container.decode(Bool.self, forKey: .expandAllContentWarnings)
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)
@ -90,11 +92,9 @@ 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,18 +124,16 @@ 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
private enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case theme case theme
case avatarStyle case avatarStyle
case hideCustomEmojiInUsernames case hideCustomEmojiInUsernames
@ -156,11 +154,9 @@ 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

View File

@ -11,11 +11,10 @@ import Gifu
import Pachyderm import Pachyderm
import AVFoundation import AVFoundation
protocol LargeImageContentView: UIView { protocol LargeImageContentView {
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 {
@ -30,14 +29,6 @@ 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
@ -59,23 +50,6 @@ 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 {
@ -111,8 +85,4 @@ 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
}
} }

View File

@ -10,6 +10,8 @@ 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 }
@ -29,7 +31,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: LargeImageContentView { var contentView: ContentView {
didSet { didSet {
oldValue.removeFromSuperview() oldValue.removeFromSuperview()
setupContentView() setupContentView()
@ -48,8 +50,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
var shrinkGestureEnabled = true var shrinkGestureEnabled = true
private var prevZoomScale: CGFloat? var prevZoomScale: CGFloat?
private var isGrayscale = false
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return true
@ -62,7 +63,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
return !controlsVisible return !controlsVisible
} }
init(contentView: LargeImageContentView, description: String?, sourceView: UIImageView?) { init(contentView: ContentView, description: String?, sourceView: UIImageView?) {
self.imageDescription = description self.imageDescription = description
self.animationSourceView = sourceView self.animationSourceView = sourceView
@ -102,8 +103,6 @@ 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() {
@ -149,13 +148,6 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
} }
} }
@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
if animated { if animated {

View File

@ -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, url: url) createLargeImage(data: data)
} 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!, url: self.url) self.createLargeImage(data: data!)
} }
} }
} }
@ -115,22 +115,13 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
} }
} }
private func createLargeImage(data: Data, url: URL) { private func createLargeImage(data: Data) {
guard !loaded else { return } guard !loaded else { return }
loaded = true loaded = true
guard let image = UIImage(data: data) else { return }
let image: UIImage?
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 let gifData = url.pathExtension == "gif" ? data : nil
createLargeImage(image: image, gifData: gifData) createLargeImage(image: image, gifData: gifData)
} }
}
private func createLargeImage(image: UIImage, gifData: Data?) { private func createLargeImage(image: UIImage, gifData: Data?) {
let imageView = LargeImageImageContentView(image: image, gifData: gifData) let imageView = LargeImageImageContentView(image: image, gifData: gifData)
@ -147,13 +138,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
private func createPreview() { private func createPreview() {
guard !self.loaded, guard !self.loaded,
var image = animationSourceView?.image else { return } let 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)
} }

View File

@ -36,16 +36,12 @@ 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")
} }
NavigationLink(destination: OppositeCollapseKeywordsView()) { Toggle(isOn: $preferences.collapseLongPosts) {
Text(preferences.expandAllContentWarnings ? "Collapse Posts with Keywords in CWs" : "Expand Posts with Keywords in CWs") Text("Collapse Long Posts")
} }
} }
} }

View File

@ -1,105 +0,0 @@
//
// 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()
}
}

View File

@ -9,28 +9,31 @@
import SwiftUI import SwiftUI
struct WellnessPrefsView: View { struct WellnessPrefsView: View {
@ObservedObject private var preferences = Preferences.shared @ObservedObject var preferences = Preferences.shared
var body: some View { var body: some View {
List { List {
showFavAndReblogCount showFavAndReblogCountSection
notificationsMode notificationsModeSection
grayscaleImages
} }
.insetOrGroupedListStyle() .insetOrGroupedListStyle()
.navigationBarTitle(Text("Digital Wellness")) .navigationBarTitle(Text("Digital Wellness"))
} }
private var showFavAndReblogCount: some View { var showFavAndReblogCountSection: some View {
Section(footer: Text("Control whether total favorite and reblog counts are shown for the main post in conversations.")) { Section(footer: showFavAndReblogCountFooter) {
Toggle(isOn: $preferences.showFavoriteAndReblogCounts) { Toggle(isOn: $preferences.showFavoriteAndReblogCounts) {
Text("Show Favorite and Reblog Counts") Text("Show Favorite and Reblog Counts")
} }
} }
} }
private var notificationsMode: some View { var showFavAndReblogCountFooter: some View {
Section(footer: Text("Choose which kinds of notifications will be shown by default in the Notifications tab.")) { Text("Control whether total favorite and reblog counts are shown for the main post in conversations.")
}
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)
@ -39,12 +42,8 @@ struct WellnessPrefsView: View {
} }
} }
private var grayscaleImages: some View { var notificationsModeFooter: some View {
Section(footer: Text("Show attachments, avatars, headers, and custom emoji in black and white.")) { Text("Choose which kinds of notifications will be shown by default in the Notifications tab.")
Toggle(isOn: $preferences.grayscaleImages) {
Text("Grayscale Images")
}
}
} }
} }

View File

@ -40,21 +40,8 @@ class MyProfileViewController: ProfileViewController {
} }
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) { private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {
let avatarURL = account.avatar _ = ImageCache.avatars.get(account.avatar, completion: { [weak self] (data) in
_ = ImageCache.avatars.get(avatarURL, completion: { [weak self] (data) in guard let self = self, let data = data, let image = UIImage(data: data) else { return }
guard let self = self, let data = data 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)

View File

@ -21,8 +21,7 @@ class AccountTableViewCell: UITableViewCell {
var accountID: String! var accountID: String!
private var avatarRequest: ImageCache.Request? var avatarRequest: ImageCache.Request?
private var isGrayscale = false
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -39,10 +38,6 @@ 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) {
@ -51,37 +46,21 @@ class AccountTableViewCell: UITableViewCell {
fatalError("Missing cached account \(accountID)") fatalError("Missing cached account \(accountID)")
} }
usernameLabel.text = "@\(account.acct)" avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
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 = image self.avatarImageView.image = UIImage(data: data)
} }
} }
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() {

View File

@ -28,22 +28,12 @@ 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)
init(attachment: Attachment, index: Int, expectedSize: CGSize) { init(attachment: Attachment, index: Int, expectedSize: CGSize) {
@ -65,29 +55,19 @@ class AttachmentView: UIImageView, GIFAnimatable {
commonInit() commonInit()
} }
private func commonInit() { 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(preferencesChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), 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 private func preferencesChanged() { @objc func gifPlaybackModeChanged() {
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,
@ -126,22 +106,16 @@ class AttachmentView: UIImageView, GIFAnimatable {
} else { } else {
size = self.expectedSize size = self.expectedSize
} }
if let preview = UIImage(blurHash: hash, size: size) {
guard var preview = UIImage(blurHash: hash, size: size) else {
return
}
if Preferences.shared.grayscaleImages,
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
preview = grayscale
}
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self = self, self.image == nil else { return } guard let self = self else { return }
if self.image == nil {
self.image = preview self.image = preview
} }
} }
} }
}
}
switch attachment.kind { switch attachment.kind {
case .image: case .image:
@ -158,36 +132,35 @@ class AttachmentView: UIImageView, GIFAnimatable {
} }
func loadImage() { func loadImage() {
let attachmentURL = attachment.url attachmentRequest = ImageCache.attachments.get(attachment.url) { [weak self] (data) in
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
if self.attachment.url.pathExtension == "gif" {
self.source = .gifData(attachmentURL, data)
if self.autoplayGifs {
DispatchQueue.main.async { DispatchQueue.main.async {
if self.attachment.url.pathExtension == "gif" {
self.gifData = data
if self.autoplayGifs {
self.animate(withGIFData: data) self.animate(withGIFData: data)
} else {
self.image = UIImage(data: data)
} }
} else { } else {
self.displayImage() self.image = UIImage(data: data)
} }
} 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 }
self.source = .cgImage(attachmentURL, image) DispatchQueue.main.async { [weak self] in
self.displayImage() guard let self = self, self.attachment.url == attachmentURL else { return }
self.image = UIImage(cgImage: image)
}
} }
let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill")) let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
@ -229,8 +202,10 @@ 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 }
self.source = .cgImage(attachmentURL, image) DispatchQueue.main.async { [weak self] in
self.displayImage() guard let self = self, self.attachment.url == attachmentURL else { return }
self.image = UIImage(cgImage: image)
}
} }
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill) let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
@ -248,35 +223,6 @@ 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)
@ -296,14 +242,6 @@ 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

View File

@ -19,16 +19,12 @@ class GifvAttachmentView: UIView {
layer as! AVPlayerLayer layer as! AVPlayerLayer
} }
private var asset: AVAsset let item: AVPlayerItem
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) {
self.asset = asset item = AVPlayerItem(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)
@ -37,39 +33,13 @@ 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")
} }
private static func createItem(asset: AVAsset) -> AVPlayerItem { @objc func restartItem() {
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()

View File

@ -45,19 +45,11 @@ 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 else { guard let data = data, let image = UIImage(data: data) else {
return return
} }
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 emojiImages[emoji.shortcode] = image
} }
}
if let request = request { if let request = request {
emojiRequests.append(request) emojiRequests.append(request)
} }

View File

@ -59,20 +59,12 @@ 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 else { guard let data = data, let image = UIImage(data: data) else {
return return
} }
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 emojiImages[emoji.shortcode] = image
} }
} }
}
group.notify(queue: .main) { group.notify(queue: .main) {
let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!) let mutAttrString = NSMutableAttributedString(attributedString: self.attributedText!)

View File

@ -1,73 +0,0 @@
//
// 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?()
}
}
}

View File

@ -25,9 +25,8 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
var group: NotificationGroup! var group: NotificationGroup!
var statusID: String! var statusID: String!
private var avatarRequests = [String: ImageCache.Request]() var avatarRequests = [String: ImageCache.Request]()
private var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -45,10 +44,6 @@ 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) {
@ -72,8 +67,6 @@ 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() }
@ -83,22 +76,11 @@ 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
let avatarURL = account.avatar avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
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 }
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 { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image imageView.image = UIImage(data: data)
}
} }
} }
actionAvatarStackView.addArrangedSubview(imageView) actionAvatarStackView.addArrangedSubview(imageView)
@ -122,38 +104,7 @@ class ActionNotificationGroupTableViewCell: UITableViewCell {
statusContentLabel.text = try! doc.text() statusContentLabel.text = try! doc.text()
} }
private func updateGrayscaleableUI() { func updateTimestamp() {
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")
} }

View File

@ -20,9 +20,8 @@ class FollowNotificationGroupTableViewCell: UITableViewCell {
var group: NotificationGroup! var group: NotificationGroup!
private var avatarRequests = [String: ImageCache.Request]() var avatarRequests = [String: ImageCache.Request]()
private var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -40,10 +39,6 @@ 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) {
@ -56,30 +51,17 @@ 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
let avatarURL = account.avatar avatarRequests[account.id] = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
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 }
let image: UIImage?
if Preferences.shared.grayscaleImages {
image = ImageGrayscalifier.convert(url: avatarURL, data: data)
} else {
image = UIImage(data: data)
}
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarRequests.removeValue(forKey: account.id) self.avatarRequests.removeValue(forKey: account.id)
imageView.image = image imageView.image = UIImage(data: data)
}
} }
} }
avatarStackView.addArrangedSubview(imageView) avatarStackView.addArrangedSubview(imageView)
@ -90,39 +72,6 @@ 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 ")

View File

@ -25,9 +25,8 @@ class FollowRequestNotificationTableViewCell: UITableViewCell {
var notification: Pachyderm.Notification? var notification: Pachyderm.Notification?
var account: Account! var account: Account!
private var avatarRequest: ImageCache.Request? var avatarRequest: ImageCache.Request?
private var updateTimestampWorkItem: DispatchWorkItem? var updateTimestampWorkItem: DispatchWorkItem?
private var isGrayscale = false
deinit { deinit {
updateTimestampWorkItem?.cancel() updateTimestampWorkItem?.cancel()
@ -44,11 +43,6 @@ 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) {
@ -67,27 +61,16 @@ 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)
} }
let avatarURL = account.avatar avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
avatarRequest = ImageCache.avatars.get(avatarURL) { [weak self] (data) in guard let self = self, self.account == account, let data = data, let image = UIImage(data: data) else { return }
guard let self = self, self.account == account, let data = data 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)
}
if let image = image {
DispatchQueue.main.async { DispatchQueue.main.async {
self.avatarImageView.image = image self.avatarImageView.image = image
} }
} }
} }
}
private func updateTimestamp() { func updateTimestamp() {
guard let notification = notification else { return } guard let notification = notification else { return }
timestampLabel.text = notification.createdAt.timeAgoString() timestampLabel.text = notification.createdAt.timeAgoString()

View File

@ -48,8 +48,6 @@ 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 {
@ -108,7 +106,22 @@ class ProfileHeaderView: UIView {
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
updateImages(account: account) avatarRequest = ImageCache.avatars.get(account.avatar) { [weak self] (data) in
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))
@ -180,49 +193,6 @@ 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

View File

@ -74,8 +74,6 @@ 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()
@ -98,8 +96,6 @@ 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() {
@ -146,7 +142,6 @@ 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
@ -159,6 +154,8 @@ 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 {
@ -218,6 +215,12 @@ 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() {
@ -229,13 +232,10 @@ 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,31 +253,6 @@ 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()

View File

@ -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" 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" 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">
<rect key="frame" x="0.0" y="58" width="343" height="20.5"/> <rect key="frame" x="0.0" y="58" width="137.5" 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" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8r8-O8-Agh">
<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,7 +216,6 @@
<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"/>

View File

@ -26,7 +26,6 @@ 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!
@ -109,51 +108,19 @@ 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
updateGrayscaleableUI(card: card) if let image = card.image {
updateUIForPreferences()
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title
titleLabel.isHidden = title.isEmpty
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
descriptionLabel.text = description
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 placeholderImageView.isHidden = true
imageRequest = ImageCache.attachments.get(imageURL, completion: { (data) in imageRequest = ImageCache.attachments.get(image, completion: { (data) in
guard let data = data else { return } guard let data = data, let image = UIImage(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 { DispatchQueue.main.async {
self.imageView.image = image self.imageView.image = image
} }
}
}) })
if imageRequest != nil { if imageRequest != nil {
loadBlurHash() loadBlurHash()
@ -161,6 +128,14 @@ class StatusCardView: UIView {
} else { } else {
placeholderImageView.isHidden = false placeholderImageView.isHidden = false
} }
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title
titleLabel.isHidden = title.isEmpty
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty
} }
private func loadBlurHash() { private func loadBlurHash() {

View File

@ -1,25 +0,0 @@
//
// 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)
}
}

View File

@ -94,8 +94,8 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
pinImageView.isHidden = !pinned pinImageView.isHidden = !pinned
} }
override func updateGrayscaleableUI(account: AccountMO, status: StatusMO) { override func updateUIForPreferences(account: AccountMO, status: StatusMO) {
super.updateGrayscaleableUI(account: account, status: status) super.updateUIForPreferences(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) {

View File

@ -84,17 +84,16 @@
<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" 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" 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">
<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" customClass="StatusCollapseButton" customModule="Tusker" customModuleProvider="target"> <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="252" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="O0E-Vf-XYR">
<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>