Merge branch 'release/0.8.2'
This commit is contained in:
commit
37b9be38c5
|
@ -273,7 +273,6 @@
|
|||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; };
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; };
|
||||
DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; };
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; };
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; };
|
||||
DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; };
|
||||
DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; };
|
||||
|
@ -296,6 +295,7 @@
|
|||
DB6180F626391D580018D199 /* MediaPreviewableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */; };
|
||||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */; };
|
||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
|
||||
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */; };
|
||||
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; };
|
||||
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
|
||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
|
||||
|
@ -921,6 +921,7 @@
|
|||
DB6180F526391D580018D199 /* MediaPreviewableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewableViewController.swift; sourceTree = "<group>"; };
|
||||
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewingViewController.swift; sourceTree = "<group>"; };
|
||||
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = "<group>"; };
|
||||
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewController+StatusProvider.swift"; sourceTree = "<group>"; };
|
||||
DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
|
||||
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -1144,7 +1145,6 @@
|
|||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */,
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||
DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */,
|
||||
|
@ -2369,6 +2369,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */,
|
||||
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */,
|
||||
2D607AD726242FC500B70763 /* NotificationViewModel.swift */,
|
||||
2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */,
|
||||
2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */,
|
||||
|
@ -2674,7 +2675,6 @@
|
|||
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
|
||||
2D42FF6025C8177C004A627A /* ActiveLabel */,
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
|
||||
DB5086B725CC0D6400C2C187 /* Kingfisher */,
|
||||
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
|
||||
2D939AC725EE14620076FA61 /* CropViewController */,
|
||||
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
|
||||
|
@ -2868,7 +2868,6 @@
|
|||
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
|
||||
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
|
||||
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
|
||||
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
|
||||
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
|
||||
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
|
||||
|
@ -3177,6 +3176,7 @@
|
|||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */,
|
||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
|
||||
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||
|
@ -3843,7 +3843,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3851,7 +3851,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -3870,7 +3870,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -3878,7 +3878,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4198,7 +4198,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4206,7 +4206,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4312,7 +4312,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4320,7 +4320,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4431,7 +4431,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4439,7 +4439,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4545,7 +4545,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4553,7 +4553,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4599,7 +4599,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4607,7 +4607,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4622,7 +4622,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
CURRENT_PROJECT_VERSION = 27;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -4630,7 +4630,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.8.0;
|
||||
MARKETING_VERSION = 0.8.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -4778,7 +4778,7 @@
|
|||
repositoryURL = "https://github.com/TwidereProject/MetaTextView.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.2.3;
|
||||
version = 1.2.4;
|
||||
};
|
||||
};
|
||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
|
||||
|
@ -4797,14 +4797,6 @@
|
|||
minimumVersion = 4.1.0;
|
||||
};
|
||||
};
|
||||
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 6.1.0;
|
||||
};
|
||||
};
|
||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git";
|
||||
|
@ -4934,11 +4926,6 @@
|
|||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||
productName = AlamofireImage;
|
||||
};
|
||||
DB5086B725CC0D6400C2C187 /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
productName = Kingfisher;
|
||||
};
|
||||
DB68050F2637D0F800430867 /* KeychainAccess */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>27</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>26</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -114,8 +114,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/MetaTextView.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5b86b386464be8a6da5383aa714c458c07da6c01",
|
||||
"version": "1.2.3"
|
||||
"revision": "28e53130d16f12e0eeb479d83b77a0a718ef2088",
|
||||
"version": "1.2.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -279,7 +279,10 @@ private extension SceneCoordinator {
|
|||
scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
viewController = SFSafariViewController(url: url)
|
||||
let _viewController = SFSafariViewController(url: url)
|
||||
_viewController.preferredControlTintColor = Asset.Colors.brandBlue.color
|
||||
viewController = _viewController
|
||||
|
||||
case .alertController(let alertController):
|
||||
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||
assert(
|
||||
|
|
|
@ -11,6 +11,7 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import Nuke
|
||||
|
||||
enum NotificationSection: Equatable, Hashable {
|
||||
case main
|
||||
|
@ -30,7 +31,9 @@ extension NotificationSection {
|
|||
guard let dependency = dependency else { return nil }
|
||||
switch notificationItem {
|
||||
case .notification(let objectID, let attribute):
|
||||
let notification = managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: notification.typeRaw) else {
|
||||
// filter out invalid type using predicate
|
||||
assertionFailure()
|
||||
|
@ -60,7 +63,7 @@ extension NotificationSection {
|
|||
statusItemAttribute: attribute
|
||||
)
|
||||
cell.actionImageBackground.backgroundColor = color
|
||||
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
timestampUpdatePublisher
|
||||
.sink { [weak cell] _ in
|
||||
|
@ -70,10 +73,13 @@ extension NotificationSection {
|
|||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
if let url = notification.account.avatarImageURL() {
|
||||
cell.avatarImageView.af.setImage(
|
||||
withURL: url,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
cell.avatarImageViewTask = Nuke.loadImage(
|
||||
with: url,
|
||||
options: ImageLoadingOptions(
|
||||
placeholder: UIImage.placeholder(color: .systemFill),
|
||||
transition: .fadeIn(duration: 0.2)
|
||||
),
|
||||
into: cell.avatarImageView
|
||||
)
|
||||
}
|
||||
cell.avatarImageView.gesture().sink { [weak cell] _ in
|
||||
|
@ -109,15 +115,18 @@ extension NotificationSection {
|
|||
.store(in: &cell.disposeBag)
|
||||
cell.actionImageBackground.backgroundColor = color
|
||||
cell.actionLabel.text = actionText + " · " + timeText
|
||||
cell.nameLabel.text = notification.account.displayName.isEmpty ? notification.account.username : notification.account.displayName
|
||||
cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict)
|
||||
if let url = notification.account.avatarImageURL() {
|
||||
cell.avatatImageView.af.setImage(
|
||||
withURL: url,
|
||||
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||
imageTransition: .crossDissolve(0.2)
|
||||
cell.avatarImageViewTask = Nuke.loadImage(
|
||||
with: url,
|
||||
options: ImageLoadingOptions(
|
||||
placeholder: UIImage.placeholder(color: .systemFill),
|
||||
transition: .fadeIn(duration: 0.2)
|
||||
),
|
||||
into: cell.avatarImageView
|
||||
)
|
||||
}
|
||||
cell.avatatImageView.gesture().sink { [weak cell] _ in
|
||||
cell.avatarImageView.gesture().sink { [weak cell] _ in
|
||||
cell?.delegate?.userAvatarDidPressed(notification: notification)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
|
|
@ -67,13 +67,13 @@ extension PollSection {
|
|||
cell.pollOptionView.checkmarkBackgroundView.isHidden = true
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .off:
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.systemGray3.cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = Asset.Colors.Background.Cell.highlight.color.withAlphaComponent(0.3).cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
cell.pollOptionView.checkmarkImageView.isHidden = true
|
||||
case .on:
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = .systemBackground
|
||||
cell.pollOptionView.checkmarkBackgroundView.backgroundColor = Asset.Colors.Background.tertiarySystemBackground.color
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderColor = UIColor.clear.cgColor
|
||||
cell.pollOptionView.checkmarkBackgroundView.layer.borderWidth = 0
|
||||
cell.pollOptionView.checkmarkBackgroundView.isHidden = false
|
||||
|
|
|
@ -99,6 +99,21 @@ extension StatusSection {
|
|||
)
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
cell.isAccessibilityElement = true
|
||||
// FIXME:
|
||||
cell.accessibilityLabel = {
|
||||
var accessibilityViews: [UIView?] = []
|
||||
if !cell.statusView.headerContainerView.isHidden {
|
||||
accessibilityViews.append(cell.statusView.headerInfoLabel)
|
||||
}
|
||||
accessibilityViews.append(contentsOf: [
|
||||
cell.statusView.nameLabel,
|
||||
cell.statusView.dateLabel,
|
||||
cell.statusView.contentMetaText.textView,
|
||||
])
|
||||
return accessibilityViews
|
||||
.compactMap { $0?.accessibilityLabel }
|
||||
.joined(separator: " ")
|
||||
}()
|
||||
return cell
|
||||
case .status(let objectID, let attribute),
|
||||
.root(let objectID, let attribute),
|
||||
|
@ -244,6 +259,7 @@ extension StatusSection {
|
|||
// set timestamp
|
||||
let createdAt = (status.reblog ?? status).createdAt
|
||||
cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow
|
||||
cell.statusView.dateLabel.accessibilityValue = createdAt.timeAgoSinceNow
|
||||
AppContext.shared.timestampUpdatePublisher
|
||||
.receive(on: RunLoop.main) // will be paused when scrolling (on purpose)
|
||||
.sink { [weak cell] _ in
|
||||
|
@ -503,6 +519,7 @@ extension StatusSection {
|
|||
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.headerInfoLabel.accessibilityLabel = headerText
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
} else if status.inReplyToID != nil {
|
||||
cell.statusView.headerContainerView.isHidden = false
|
||||
|
@ -522,7 +539,8 @@ extension StatusSection {
|
|||
cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = true
|
||||
cell.statusView.headerInfoLabel.accessibilityLabel = headerText
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = status.replyTo != nil
|
||||
} else {
|
||||
cell.statusView.headerContainerView.isHidden = true
|
||||
cell.statusView.headerInfoLabel.isAccessibilityElement = false
|
||||
|
@ -536,7 +554,14 @@ extension StatusSection {
|
|||
// name
|
||||
let author = (status.reblog ?? status).author
|
||||
let nameContent = author.displayNameWithFallback
|
||||
cell.statusView.nameLabel.configure(content: nameContent, emojiDict: author.emojiDict)
|
||||
MastodonStatusContent.parseResult(content: nameContent, emojiDict: author.emojiDict)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] parseResult in
|
||||
guard let cell = cell else { return }
|
||||
cell.statusView.nameLabel.configure(contentParseResult: parseResult)
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
cell.statusView.nameLabel.accessibilityLabel = nameContent
|
||||
// username
|
||||
cell.statusView.usernameLabel.text = "@" + author.acct
|
||||
// avatar
|
||||
|
@ -560,17 +585,22 @@ extension StatusSection {
|
|||
) {
|
||||
// set content
|
||||
do {
|
||||
let status = status.reblog ?? status
|
||||
let content = MastodonContent(
|
||||
content: (status.reblog ?? status).content,
|
||||
emojis: (status.reblog ?? status).emojiMeta
|
||||
content: status.content,
|
||||
emojis: status.emojiMeta
|
||||
)
|
||||
let metaContent = try MastodonMetaContent.convert(document: content)
|
||||
cell.statusView.contentMetaText.configure(content: metaContent)
|
||||
cell.statusView.contentMetaText.textView.accessibilityLabel = metaContent.trimmed
|
||||
} catch {
|
||||
cell.statusView.contentMetaText.textView.text = " "
|
||||
cell.statusView.contentMetaText.textView.accessibilityLabel = ""
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
cell.statusView.contentMetaText.textView.accessibilityTraits = [.staticText]
|
||||
cell.statusView.contentMetaText.textView.accessibilityElementsHidden = false
|
||||
cell.statusView.contentMetaText.textView.accessibilityLanguage = (status.reblog ?? status).language
|
||||
|
||||
// set visibility
|
||||
|
@ -633,46 +663,19 @@ extension StatusSection {
|
|||
|
||||
let isSingleMosaicLayout = mosaics.count == 1
|
||||
|
||||
// set link preview
|
||||
// cell.statusView.linkPreview.isHidden = true
|
||||
//
|
||||
// var _firstURL: URL? = {
|
||||
// for entity in cell.statusView.activeTextLabel.activeEntities {
|
||||
// guard case let .url(_, _, url, _) = entity.type else { continue }
|
||||
// return URL(string: url)
|
||||
// }
|
||||
// return nil
|
||||
// }()
|
||||
//
|
||||
// if let url = _firstURL {
|
||||
// Future<LPLinkMetadata?, Error> { promise in
|
||||
// LPMetadataProvider().startFetchingMetadata(for: url) { meta, error in
|
||||
// if let error = error {
|
||||
// promise(.failure(error))
|
||||
// } else {
|
||||
// promise(.success(meta))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .receive(on: RunLoop.main)
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { [weak cell] meta in
|
||||
// guard let meta = meta else { return }
|
||||
// guard let cell = cell else { return }
|
||||
// cell.statusView.linkPreview.metadata = meta
|
||||
// cell.statusView.linkPreview.isHidden = false
|
||||
// }
|
||||
// .store(in: &cell.disposeBag)
|
||||
// }
|
||||
|
||||
// set image
|
||||
let imageSize = CGSize(
|
||||
width: mosaic.imageViewSize.width * imageView.traitCollection.displayScale,
|
||||
height: mosaic.imageViewSize.height * imageView.traitCollection.displayScale
|
||||
)
|
||||
let url: URL? = {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return meta.previewURL ?? meta.url
|
||||
}
|
||||
return meta.url
|
||||
}()
|
||||
let request = ImageRequest(
|
||||
url: meta.url,
|
||||
url: url,
|
||||
processors: [
|
||||
ImageProcessors.Resize(
|
||||
size: imageSize,
|
||||
|
|
|
@ -57,6 +57,15 @@ extension ActiveLabel {
|
|||
|
||||
}
|
||||
|
||||
extension ActiveLabel {
|
||||
func configure(text: String) {
|
||||
attributedText = nil
|
||||
activeEntities.removeAll()
|
||||
self.text = text
|
||||
accessibilityLabel = text
|
||||
}
|
||||
}
|
||||
|
||||
extension ActiveLabel {
|
||||
|
||||
/// status content
|
||||
|
|
|
@ -7,12 +7,23 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
// MARK: - Convinience view creation method
|
||||
// MARK: - Convenience view creation method
|
||||
extension UIView {
|
||||
|
||||
static let separatorColor: UIColor = {
|
||||
UIColor(dynamicProvider: { collection in
|
||||
switch collection.userInterfaceStyle {
|
||||
case .dark:
|
||||
return Asset.Colors.Background.Cell.separator.color
|
||||
default:
|
||||
return .separator
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
static var separatorLine: UIView {
|
||||
let line = UIView()
|
||||
line.backgroundColor = .separator
|
||||
line.backgroundColor = UIView.separatorColor
|
||||
return line
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,10 @@ internal enum Asset {
|
|||
}
|
||||
internal enum Colors {
|
||||
internal enum Background {
|
||||
internal enum Cell {
|
||||
internal static let highlight = ColorAsset(name: "Colors/Background/Cell/highlight")
|
||||
internal static let separator = ColorAsset(name: "Colors/Background/Cell/separator")
|
||||
}
|
||||
internal enum Poll {
|
||||
internal static let disabled = ColorAsset(name: "Colors/Background/Poll/disabled")
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ enum MastodonStatusContent {
|
|||
typealias EmojiShortcode = String
|
||||
typealias EmojiDict = [EmojiShortcode: URL]
|
||||
|
||||
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive)
|
||||
static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent)
|
||||
|
||||
static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher<MastodonStatusContent.ParseResult?, Never> {
|
||||
return Future { promise in
|
||||
|
|
|
@ -16,6 +16,17 @@
|
|||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>org.joinmastodon.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>mastodon</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
|
|
@ -14,27 +14,27 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
// prefetch reply status
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
|
||||
var statusObjectIDs: [NSManagedObjectID] = []
|
||||
for item in items(indexPaths: indexPaths) {
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
let homeTimelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
statusObjectIDs.append(homeTimelineIndex.status.objectID)
|
||||
case .status(let objectID, _):
|
||||
statusObjectIDs.append(objectID)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundManagedObjectContext = context.backgroundManagedObjectContext
|
||||
backgroundManagedObjectContext.perform { [weak self] in
|
||||
let items = self.items(indexPaths: indexPaths)
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
managedObjectContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
for objectID in statusObjectIDs {
|
||||
let status = backgroundManagedObjectContext.object(with: objectID) as! Status
|
||||
|
||||
// fetch in-reply info if needs
|
||||
|
||||
var statuses: [Status] = []
|
||||
for item in items {
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _):
|
||||
guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else { continue }
|
||||
statuses.append(homeTimelineIndex.status)
|
||||
case .status(let objectID, _):
|
||||
guard let status = try? managedObjectContext.existingObject(with: objectID) as? Status else { continue }
|
||||
statuses.append(status)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for status in statuses {
|
||||
if let replyToID = status.inReplyToID, status.replyTo == nil {
|
||||
self.context.statusPrefetchingService.prefetchReplyTo(
|
||||
domain: domain,
|
||||
|
@ -44,12 +44,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
authorizationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
|
||||
// self.context.statusContentCacheService.prefetch(
|
||||
// content: (status.reblog ?? status).content,
|
||||
// emojiDict: (status.reblog ?? status).emojiDict
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end for in
|
||||
} // end context.perform
|
||||
} // end func
|
||||
}
|
||||
|
|
|
@ -523,32 +523,6 @@ extension StatusProviderFacade {
|
|||
|
||||
extension StatusProviderFacade {
|
||||
|
||||
static func responseToStatusContentWarningRevealAction(dependency: NotificationViewController, cell: UITableViewCell) {
|
||||
let status = Future<Status?, Never> { promise in
|
||||
guard let diffableDataSource = dependency.viewModel.diffableDataSource,
|
||||
let indexPath = dependency.tableView.indexPath(for: cell),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .notification(let objectID, _):
|
||||
dependency.viewModel.fetchedResultsController.managedObjectContext.perform {
|
||||
let notification = dependency.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||
promise(.success(notification.status))
|
||||
}
|
||||
default:
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
|
||||
_responseToStatusContentWarningRevealAction(
|
||||
dependency: dependency,
|
||||
status: status
|
||||
)
|
||||
}
|
||||
|
||||
static func responseToStatusContentWarningRevealAction(provider: StatusProvider, cell: UITableViewCell) {
|
||||
_responseToStatusContentWarningRevealAction(
|
||||
dependency: provider,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "214",
|
||||
"green" : "209",
|
||||
"red" : "209"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x6E",
|
||||
"green" : "0x57",
|
||||
"red" : "0x4F"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.839",
|
||||
"green" : "0.820",
|
||||
"red" : "0.820"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.431",
|
||||
"green" : "0.341",
|
||||
"red" : "0.310"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "67",
|
||||
"green" : "53",
|
||||
"red" : "49"
|
||||
"blue" : "0x43",
|
||||
"green" : "0x35",
|
||||
"red" : "0x31"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "55",
|
||||
"green" : "44",
|
||||
"red" : "40"
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "55",
|
||||
"green" : "44",
|
||||
"red" : "40"
|
||||
"blue" : "0x37",
|
||||
"green" : "0x2C",
|
||||
"red" : "0x28"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x3C",
|
||||
"green" : "0x3A",
|
||||
"red" : "0x3A"
|
||||
"blue" : "0.216",
|
||||
"green" : "0.173",
|
||||
"red" : "0.157"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x3C",
|
||||
"green" : "0x3A",
|
||||
"red" : "0x3A"
|
||||
"blue" : "0.263",
|
||||
"green" : "0.208",
|
||||
"red" : "0.192"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -12,7 +12,7 @@ final class AutoCompleteTableViewCell: UITableViewCell {
|
|||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
static let avatarToLabelSpacing: CGFloat = 12
|
||||
|
||||
|
||||
let containerStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
|
@ -47,12 +47,6 @@ final class AutoCompleteTableViewCell: UITableViewCell {
|
|||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatarImageView.af.cancelImageRequest()
|
||||
avatarImageView.kf.cancelDownloadTask()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
|
@ -80,6 +74,11 @@ extension AutoCompleteTableViewCell {
|
|||
|
||||
private func _init() {
|
||||
backgroundColor = .clear
|
||||
selectedBackgroundView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.Cell.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let topPaddingView = UIView()
|
||||
let bottomPaddingView = UIView()
|
||||
|
|
|
@ -9,12 +9,11 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import PhotosUI
|
||||
import Kingfisher
|
||||
import MastodonSDK
|
||||
import TwitterTextEditor
|
||||
import MetaTextView
|
||||
import MastodonMeta
|
||||
import Meta
|
||||
import Nuke
|
||||
|
||||
final class ComposeViewController: UIViewController, NeedsDependency {
|
||||
|
||||
|
@ -788,147 +787,6 @@ extension ComposeViewController: UITextViewDelegate {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - TextEditorViewTextAttributesDelegate
|
||||
extension ComposeViewController: TextEditorViewTextAttributesDelegate {
|
||||
|
||||
func textEditorView(
|
||||
_ textEditorView: TextEditorView,
|
||||
updateAttributedString attributedString: NSAttributedString,
|
||||
completion: @escaping (NSAttributedString?) -> Void
|
||||
) {
|
||||
// FIXME: needs O(1) update completion to fix performance issue
|
||||
DispatchQueue.global().async {
|
||||
let string = attributedString.string
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update: %s", ((#file as NSString).lastPathComponent), #line, #function, string)
|
||||
|
||||
let stringRange = NSRange(location: 0, length: string.length)
|
||||
let highlightMatches = string.matches(pattern: MastodonRegex.highlightPattern)
|
||||
let emojiMatches = string.matches(pattern: MastodonRegex.emojiPattern)
|
||||
// only accept http/https scheme
|
||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let customEmojiViewModel = self.viewModel.customEmojiViewModel.value
|
||||
for view in self.suffixedAttachmentViews {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
self.suffixedAttachmentViews.removeAll()
|
||||
|
||||
// set normal appearance
|
||||
let attributedString = NSMutableAttributedString(attributedString: attributedString)
|
||||
attributedString.removeAttribute(.suffixedAttachment, range: stringRange)
|
||||
attributedString.removeAttribute(.underlineStyle, range: stringRange)
|
||||
attributedString.addAttribute(.foregroundColor, value: Asset.Colors.Label.primary.color, range: stringRange)
|
||||
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: stringRange)
|
||||
|
||||
// hashtag
|
||||
for match in highlightMatches {
|
||||
// set highlight
|
||||
var attributes = [NSAttributedString.Key: Any]()
|
||||
attributes[.foregroundColor] = Asset.Colors.brandBlue.color
|
||||
|
||||
// See `traitCollectionDidChange(_:)`
|
||||
// set accessibility
|
||||
if #available(iOS 13.0, *) {
|
||||
switch self.traitCollection.accessibilityContrast {
|
||||
case .high:
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
attributedString.addAttributes(attributes, range: match.range)
|
||||
}
|
||||
|
||||
// emoji
|
||||
if let customEmojiViewModel = customEmojiViewModel, !customEmojiViewModel.emojiDict.value.isEmpty {
|
||||
for match in emojiMatches {
|
||||
guard let name = string.substring(with: match, at: 2) else { continue }
|
||||
guard let emoji = customEmojiViewModel.emoji(shortcode: name) else { continue }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||
|
||||
// set emoji token invisible (without upper bounce space)
|
||||
var attributes = [NSAttributedString.Key: Any]()
|
||||
attributes[.font] = UIFont.systemFont(ofSize: 0.01)
|
||||
attributedString.addAttributes(attributes, range: match.range)
|
||||
|
||||
// append emoji attachment
|
||||
let imageViewSize = CGSize(width: 20, height: 20)
|
||||
let imageView = UIImageView(frame: CGRect(origin: .zero, size: imageViewSize))
|
||||
textEditorView.textContentView.addSubview(imageView)
|
||||
self.suffixedAttachmentViews.append(imageView)
|
||||
let processor = DownsamplingImageProcessor(size: imageViewSize)
|
||||
imageView.kf.setImage(
|
||||
with: URL(string: emoji.url),
|
||||
placeholder: UIImage.placeholder(size: imageViewSize, color: .systemFill),
|
||||
options: [
|
||||
.processor(processor),
|
||||
.scaleFactor(textEditorView.traitCollection.displayScale),
|
||||
], completionHandler: nil
|
||||
)
|
||||
let layoutInTextContainer = { [weak textEditorView] (view: UIView, frame: CGRect) in
|
||||
// `textEditorView` retains `textStorage`, which retains this block as a part of attributes.
|
||||
guard let textEditorView = textEditorView else {
|
||||
return
|
||||
}
|
||||
let insets = textEditorView.textContentInsets
|
||||
view.frame = frame.offsetBy(dx: insets.left, dy: insets.top)
|
||||
}
|
||||
let attachment = TextAttributes.SuffixedAttachment(
|
||||
size: imageViewSize,
|
||||
attachment: .view(view: imageView, layoutInTextContainer: layoutInTextContainer)
|
||||
)
|
||||
let index = match.range.upperBound - 1
|
||||
attributedString.addAttribute(
|
||||
.suffixedAttachment,
|
||||
value: attachment,
|
||||
range: NSRange(location: index, length: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// url
|
||||
for match in urlMatches {
|
||||
guard let name = string.substring(with: match, at: 0) else { continue }
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name)
|
||||
|
||||
// set highlight
|
||||
var attributes = [NSAttributedString.Key: Any]()
|
||||
attributes[.foregroundColor] = Asset.Colors.brandBlue.color
|
||||
|
||||
// See `traitCollectionDidChange(_:)`
|
||||
// set accessibility
|
||||
if #available(iOS 13.0, *) {
|
||||
switch self.traitCollection.accessibilityContrast {
|
||||
case .high:
|
||||
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
attributedString.addAttributes(attributes, range: match.range)
|
||||
}
|
||||
|
||||
if string.count > ComposeViewModel.composeContentLimit {
|
||||
var attributes = [NSAttributedString.Key: Any]()
|
||||
attributes[.foregroundColor] = Asset.Colors.danger.color
|
||||
let boundStart = string.index(string.startIndex, offsetBy: ComposeViewModel.composeContentLimit)
|
||||
let boundEnd = string.endIndex
|
||||
let range = boundStart..<boundEnd
|
||||
attributedString.addAttributes(attributes, range: NSRange(range, in: string))
|
||||
}
|
||||
|
||||
completion(attributedString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ComposeToolbarViewDelegate
|
||||
extension ComposeViewController: ComposeToolbarViewDelegate {
|
||||
|
||||
|
|
|
@ -18,10 +18,6 @@ import MastodonSDK
|
|||
import AlamofireImage
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if DEBUG
|
||||
import GDPerformanceView_Swift
|
||||
#endif
|
||||
|
||||
final class AsyncHomeTimelineViewController: ASDKViewController<ASTableNode>, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -107,7 +103,6 @@ extension AsyncHomeTimelineViewController {
|
|||
#if DEBUG
|
||||
// long press to trigger debug menu
|
||||
settingBarButtonItem.menu = debugMenu
|
||||
PerformanceMonitor.shared().delegate = self
|
||||
#else
|
||||
settingBarButtonItem.target = self
|
||||
settingBarButtonItem.action = #selector(AsyncHomeTimelineViewController.settingBarButtonItemPressed(_:))
|
||||
|
@ -548,13 +543,6 @@ extension AsyncHomeTimelineViewController: StatusTableViewControllerNavigateable
|
|||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension AsyncHomeTimelineViewController: PerformanceMonitorDelegate {
|
||||
func performanceMonitor(didReport performanceReport: PerformanceReport) {
|
||||
// print(performanceReport)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - ASTableDelegate
|
||||
extension AsyncHomeTimelineViewController: ASTableDelegate {
|
||||
|
|
|
@ -77,7 +77,12 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let fetchRequest = HomeTimelineIndex.sortedFetchRequest
|
||||
fetchRequest.fetchBatchSize = 20
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(HomeTimelineIndex.status)]
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = [
|
||||
#keyPath(HomeTimelineIndex.status),
|
||||
#keyPath(HomeTimelineIndex.status.author),
|
||||
#keyPath(HomeTimelineIndex.status.reblog),
|
||||
#keyPath(HomeTimelineIndex.status.reblog.author),
|
||||
]
|
||||
let controller = NSFetchedResultsController(
|
||||
fetchRequest: fetchRequest,
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
|
|
|
@ -21,7 +21,7 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency {
|
|||
var viewModel: MediaPreviewViewModel!
|
||||
|
||||
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
let pagingViewConttroller = MediaPreviewPagingViewController()
|
||||
let pagingViewController = MediaPreviewPagingViewController()
|
||||
|
||||
let closeButtonBackground: UIVisualEffectView = {
|
||||
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial))
|
||||
|
@ -57,16 +57,16 @@ extension MediaPreviewViewController {
|
|||
visualEffectView.frame = view.bounds
|
||||
view.addSubview(visualEffectView)
|
||||
|
||||
pagingViewConttroller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
addChild(pagingViewConttroller)
|
||||
visualEffectView.contentView.addSubview(pagingViewConttroller.view)
|
||||
pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
addChild(pagingViewController)
|
||||
visualEffectView.contentView.addSubview(pagingViewController.view)
|
||||
NSLayoutConstraint.activate([
|
||||
visualEffectView.topAnchor.constraint(equalTo: pagingViewConttroller.view.topAnchor),
|
||||
visualEffectView.bottomAnchor.constraint(equalTo: pagingViewConttroller.view.bottomAnchor),
|
||||
visualEffectView.leadingAnchor.constraint(equalTo: pagingViewConttroller.view.leadingAnchor),
|
||||
visualEffectView.trailingAnchor.constraint(equalTo: pagingViewConttroller.view.trailingAnchor),
|
||||
visualEffectView.topAnchor.constraint(equalTo: pagingViewController.view.topAnchor),
|
||||
visualEffectView.bottomAnchor.constraint(equalTo: pagingViewController.view.bottomAnchor),
|
||||
visualEffectView.leadingAnchor.constraint(equalTo: pagingViewController.view.leadingAnchor),
|
||||
visualEffectView.trailingAnchor.constraint(equalTo: pagingViewController.view.trailingAnchor),
|
||||
])
|
||||
pagingViewConttroller.didMove(toParent: self)
|
||||
pagingViewController.didMove(toParent: self)
|
||||
|
||||
closeButtonBackground.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(closeButtonBackground)
|
||||
|
@ -90,9 +90,9 @@ extension MediaPreviewViewController {
|
|||
|
||||
viewModel.mediaPreviewImageViewControllerDelegate = self
|
||||
|
||||
pagingViewConttroller.interPageSpacing = 10
|
||||
pagingViewConttroller.delegate = self
|
||||
pagingViewConttroller.dataSource = viewModel
|
||||
pagingViewController.interPageSpacing = 10
|
||||
pagingViewController.delegate = self
|
||||
pagingViewController.dataSource = viewModel
|
||||
|
||||
closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside)
|
||||
|
||||
|
@ -128,8 +128,8 @@ extension MediaPreviewViewController {
|
|||
// MARK: - MediaPreviewingViewController
|
||||
extension MediaPreviewViewController: MediaPreviewingViewController {
|
||||
|
||||
func isInteractiveDismissable() -> Bool {
|
||||
if let mediaPreviewImageViewController = pagingViewConttroller.currentViewController as? MediaPreviewImageViewController {
|
||||
func isInteractiveDismissible() -> Bool {
|
||||
if let mediaPreviewImageViewController = pagingViewController.currentViewController as? MediaPreviewImageViewController {
|
||||
let previewImageView = mediaPreviewImageViewController.previewImageView
|
||||
// TODO: allow zooming pan dismiss
|
||||
guard previewImageView.zoomScale == previewImageView.minimumZoomScale else {
|
||||
|
@ -138,12 +138,12 @@ extension MediaPreviewViewController: MediaPreviewingViewController {
|
|||
|
||||
let safeAreaInsets = previewImageView.safeAreaInsets
|
||||
let statusBarFrameHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
||||
let dismissable = previewImageView.contentOffset.y <= -(safeAreaInsets.top - statusBarFrameHeight)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissable %s", ((#file as NSString).lastPathComponent), #line, #function, dismissable ? "true" : "false")
|
||||
return dismissable
|
||||
let dismissible = previewImageView.contentOffset.y <= -(safeAreaInsets.top - statusBarFrameHeight) + 3 // add 3pt tolerance
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible %s", ((#file as NSString).lastPathComponent), #line, #function, dismissible ? "true" : "false")
|
||||
return dismissible
|
||||
}
|
||||
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissable false", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible false", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -303,11 +303,11 @@ extension MediaPreviewViewController {
|
|||
}
|
||||
|
||||
@objc private func showNextKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
pagingViewConttroller.scrollToPage(.next, animated: true, completion: nil)
|
||||
pagingViewController.scrollToPage(.next, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPreviousKeyCommandHandler(_ sender: UIKeyCommand) {
|
||||
pagingViewConttroller.scrollToPage(.previous, animated: true, completion: nil)
|
||||
pagingViewController.scrollToPage(.previous, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,12 @@
|
|||
import os.log
|
||||
import func AVFoundation.AVMakeRect
|
||||
import UIKit
|
||||
import FLAnimatedImage
|
||||
|
||||
final class MediaPreviewImageView: UIScrollView {
|
||||
|
||||
let imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let imageView: FLAnimatedImageView = {
|
||||
let imageView = FLAnimatedImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.clipsToBounds = true
|
||||
imageView.isUserInteractionEnabled = true
|
||||
|
@ -120,7 +121,9 @@ extension MediaPreviewImageView {
|
|||
}
|
||||
}()
|
||||
imageView.frame = CGRect(origin: .zero, size: imageViewSize)
|
||||
imageView.image = image
|
||||
if imageView.image == nil {
|
||||
imageView.image = image
|
||||
}
|
||||
contentSize = imageViewSize
|
||||
contentInset = imageContentInset
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import Nuke
|
||||
|
||||
protocol MediaPreviewImageViewControllerDelegate: AnyObject {
|
||||
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer)
|
||||
|
@ -68,7 +69,26 @@ extension MediaPreviewImageViewController {
|
|||
|
||||
let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self)
|
||||
previewImageView.addInteraction(previewImageViewContextMenuInteraction)
|
||||
|
||||
|
||||
// switch viewModel.item {
|
||||
// case .local(let meta):
|
||||
// self.previewImageView.imageView.image = meta.image
|
||||
// self.previewImageView.setup(image: meta.image, container: self.previewImageView, forceUpdate: true)
|
||||
// self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
|
||||
// case .status(let meta):
|
||||
// Nuke.loadImage(
|
||||
// with: meta.url,
|
||||
// into: self.previewImageView.imageView
|
||||
// ) { result in
|
||||
// switch result {
|
||||
// case .failure(let error):
|
||||
// break
|
||||
// case .success(let response):
|
||||
// self.previewImageView.setup(image: response.image, container: self.previewImageView, forceUpdate: true)
|
||||
// self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
viewModel.image
|
||||
.receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state)
|
||||
.sink { [weak self] image in
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import AlamofireImage
|
||||
import Nuke
|
||||
|
||||
class MediaPreviewImageViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let item: ImagePreviewItem
|
||||
|
@ -25,16 +27,20 @@ class MediaPreviewImageViewModel {
|
|||
self.altText = meta.altText
|
||||
|
||||
let url = meta.url
|
||||
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
switch response.result {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
|
||||
case .success(let image):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
|
||||
self.image.value = image
|
||||
|
||||
ImagePipeline.shared.imagePublisher(with: url)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
self.image.value = response.image
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
init(meta: LocalImagePreviewMeta) {
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// NotificationViewController+StatusProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-7-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
extension NotificationViewController: StatusProvider {
|
||||
func status() -> Future<Status?, Never> {
|
||||
return Future<Status?, Never> { promise in
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
|
||||
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future<Status?, Never> {
|
||||
return Future<Status?, Never> { promise in
|
||||
guard let cell = cell,
|
||||
let diffableDataSource = self.viewModel.diffableDataSource,
|
||||
let indexPath = self.tableView.indexPath(for: cell),
|
||||
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||
promise(.success(nil))
|
||||
return
|
||||
}
|
||||
|
||||
switch item {
|
||||
case .notification(let objectID, _):
|
||||
self.viewModel.fetchedResultsController.managedObjectContext.perform {
|
||||
let notification = self.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification
|
||||
promise(.success(notification.status))
|
||||
}
|
||||
default:
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func status(for cell: UICollectionViewCell) -> Future<Status?, Never> {
|
||||
return Future<Status?, Never> { promise in
|
||||
promise(.success(nil))
|
||||
}
|
||||
}
|
||||
|
||||
var managedObjectContext: NSManagedObjectContext {
|
||||
viewModel.fetchedResultsController.managedObjectContext
|
||||
}
|
||||
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func items(indexPaths: [IndexPath]) -> [Item] {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -12,6 +12,8 @@ import GameplayKit
|
|||
import MastodonSDK
|
||||
import OSLog
|
||||
import UIKit
|
||||
import Meta
|
||||
import MetaTextView
|
||||
|
||||
final class NotificationViewController: UIViewController, NeedsDependency {
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
|
@ -141,10 +143,9 @@ extension NotificationViewController {
|
|||
|
||||
tableView.deselectRow(with: transitionCoordinator, animated: animated)
|
||||
|
||||
// fetch latest if has unread push notification
|
||||
if context.notificationService.hasUnreadPushNotification.value {
|
||||
viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
|
||||
}
|
||||
// fetch latest notification when will appear
|
||||
viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
|
||||
|
||||
|
||||
// needs trigger manually after onboarding dismiss
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
|
@ -191,33 +192,34 @@ extension NotificationViewController {
|
|||
extension NotificationViewController: StatusTableViewControllerAspect { }
|
||||
|
||||
// MARK: - TableViewCellHeightCacheableContainer
|
||||
extension NotificationViewController: TableViewCellHeightCacheableContainer {
|
||||
var cellFrameCache: NSCache<NSNumber, NSValue> {
|
||||
viewModel.cellFrameCache
|
||||
}
|
||||
|
||||
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
let key = item.hashValue
|
||||
let frame = cell.frame
|
||||
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||
}
|
||||
//extension NotificationViewController: TableViewCellHeightCacheableContainer {
|
||||
// var cellFrameCache: NSCache<NSNumber, NSValue> {
|
||||
// viewModel.cellFrameCache
|
||||
// }
|
||||
//
|
||||
// func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
// let key = item.hashValue
|
||||
// let frame = cell.frame
|
||||
// viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||
// }
|
||||
//
|
||||
// func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
// guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension }
|
||||
// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
||||
// guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||
// if case .bottomLoader = item {
|
||||
// return TimelineLoaderTableViewCell.cellHeight
|
||||
// } else {
|
||||
// return UITableView.automaticDimension
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return ceil(frame.height)
|
||||
// }
|
||||
//}
|
||||
|
||||
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
|
||||
guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
|
||||
if case .bottomLoader = item {
|
||||
return TimelineLoaderTableViewCell.cellHeight
|
||||
} else {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
|
||||
return ceil(frame.height)
|
||||
}
|
||||
}
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension NotificationViewController: UITableViewDelegate {
|
||||
|
@ -271,6 +273,7 @@ extension NotificationViewController: ContentOffsetAdjustableTimelineViewControl
|
|||
|
||||
// MARK: - NotificationTableViewCellDelegate
|
||||
extension NotificationViewController: NotificationTableViewCellDelegate {
|
||||
|
||||
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton) {
|
||||
viewModel.acceptFollowRequest(notification: notification)
|
||||
}
|
||||
|
@ -286,20 +289,31 @@ extension NotificationViewController: NotificationTableViewCellDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
func userNameLabelDidPressed(notification: MastodonNotification) {
|
||||
let viewModel = CachedProfileViewModel(context: context, mastodonUser: notification.account)
|
||||
DispatchQueue.main.async {
|
||||
self.coordinator.present(scene: .profile(viewModel: viewModel), from: self, transition: .show)
|
||||
}
|
||||
}
|
||||
|
||||
func parent() -> UIViewController {
|
||||
self
|
||||
}
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: self, cell: cell)
|
||||
StatusProviderFacade.responseToStatusContentWarningRevealAction(provider: self, cell: cell)
|
||||
}
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
StatusProviderFacade.responseToStatusMetaTextAction(provider: self, cell: cell, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ import UIKit
|
|||
import ActiveLabel
|
||||
import MetaTextView
|
||||
import Meta
|
||||
import FLAnimatedImage
|
||||
import Nuke
|
||||
|
||||
final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
||||
static let actionImageBorderWidth: CGFloat = 2
|
||||
|
@ -18,9 +20,10 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
var pollCountdownSubscription: AnyCancellable?
|
||||
var delegate: NotificationTableViewCellDelegate?
|
||||
|
||||
|
||||
var avatarImageViewTask: ImageTask?
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let imageView = FLAnimatedImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
imageView.clipsToBounds = true
|
||||
|
@ -57,8 +60,8 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel(style: .statusName)
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
|
@ -88,12 +91,12 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatarImageView.af.cancelImageRequest()
|
||||
avatarImageViewTask?.cancel()
|
||||
avatarImageViewTask = nil
|
||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||
statusView.pollTableView.dataSource = nil
|
||||
statusView.playerContainerView.reset()
|
||||
statusView.playerContainerView.isHidden = true
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
|
@ -112,7 +115,11 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
|
|||
extension NotificationStatusTableViewCell {
|
||||
func configure() {
|
||||
backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
selectedBackgroundView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.Cell.highlight.color
|
||||
return view
|
||||
}()
|
||||
let containerStackView = UIStackView()
|
||||
containerStackView.axis = .horizontal
|
||||
containerStackView.alignment = .top
|
||||
|
@ -259,7 +266,7 @@ extension NotificationStatusTableViewCell: StatusViewDelegate {
|
|||
}
|
||||
|
||||
func statusView(_ statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
// do nothing
|
||||
delegate?.notificationStatusTableViewCell(self, statusView: statusView, metaText: metaText, didSelectMeta: meta)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,11 @@ import Combine
|
|||
import CoreDataStack
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Meta
|
||||
import MetaTextView
|
||||
import ActiveLabel
|
||||
import FLAnimatedImage
|
||||
import Nuke
|
||||
|
||||
protocol NotificationTableViewCellDelegate: AnyObject {
|
||||
var context: AppContext! { get }
|
||||
|
@ -16,13 +21,14 @@ protocol NotificationTableViewCellDelegate: AnyObject {
|
|||
func parent() -> UIViewController
|
||||
|
||||
func userAvatarDidPressed(notification: MastodonNotification)
|
||||
func userNameLabelDidPressed(notification: MastodonNotification)
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton)
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
|
||||
|
||||
func notificationStatusTableViewCell(_ cell: NotificationStatusTableViewCell, statusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
|
||||
|
||||
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, acceptButtonDidPressed button: UIButton)
|
||||
|
||||
func notificationTableViewCell(_ cell: NotificationTableViewCell, notification: MastodonNotification, rejectButtonDidPressed button: UIButton)
|
||||
|
||||
}
|
||||
|
@ -33,9 +39,10 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var delegate: NotificationTableViewCellDelegate?
|
||||
|
||||
let avatatImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
|
||||
var avatarImageViewTask: ImageTask?
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = FLAnimatedImageView()
|
||||
imageView.layer.cornerRadius = 4
|
||||
imageView.layer.cornerCurve = .continuous
|
||||
imageView.clipsToBounds = true
|
||||
|
@ -72,8 +79,8 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
return label
|
||||
}()
|
||||
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let nameLabel: ActiveLabel = {
|
||||
let label = ActiveLabel()
|
||||
label.textColor = Asset.Colors.brandBlue.color
|
||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20)
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
|
@ -108,7 +115,8 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
avatatImageView.af.cancelImageRequest()
|
||||
avatarImageViewTask?.cancel()
|
||||
avatarImageViewTask = nil
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
|
@ -126,6 +134,11 @@ final class NotificationTableViewCell: UITableViewCell {
|
|||
extension NotificationTableViewCell {
|
||||
func configure() {
|
||||
backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
selectedBackgroundView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.Cell.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
let containerStackView = UIStackView()
|
||||
containerStackView.axis = .vertical
|
||||
|
@ -153,13 +166,13 @@ extension NotificationTableViewCell {
|
|||
avatarContainer.widthAnchor.constraint(equalToConstant: 47).priority(.required - 1)
|
||||
])
|
||||
|
||||
avatarContainer.addSubview(avatatImageView)
|
||||
avatatImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addSubview(avatarImageView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
avatatImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatatImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatatImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||
avatatImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1),
|
||||
avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor),
|
||||
avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor)
|
||||
])
|
||||
|
||||
avatarContainer.addSubview(actionImageBackground)
|
||||
|
|
|
@ -171,7 +171,7 @@ extension MastodonConfirmEmailViewController {
|
|||
func showEmailAppAlert() {
|
||||
let clients = ThirdPartyMailClient.clients()
|
||||
let application = UIApplication.shared
|
||||
let avaliableClients = clients.filter { client -> Bool in
|
||||
let availableClients = clients.filter { client -> Bool in
|
||||
ThirdPartyMailer.application(application, isMailClientAvailable: client)
|
||||
}
|
||||
let alertController = UIAlertController(title: L10n.Scene.ConfirmEmail.OpenEmailApp.openEmailClient, message: nil, preferredStyle: .alert)
|
||||
|
@ -180,9 +180,9 @@ extension MastodonConfirmEmailViewController {
|
|||
UIApplication.shared.open(URL(string: "message://")!, options: [:], completionHandler: nil)
|
||||
}
|
||||
alertController.addAction(alertAction)
|
||||
_ = avaliableClients.compactMap { client -> UIAlertAction in
|
||||
_ = availableClients.compactMap { client -> UIAlertAction in
|
||||
let alertAction = UIAlertAction(title: client.name, style: .default) { _ in
|
||||
_ = ThirdPartyMailer.application(application, openMailClient: client, recipient: nil, subject: nil, body: nil)
|
||||
_ = ThirdPartyMailer.application(application, openMailClient: client)
|
||||
}
|
||||
alertController.addAction(alertAction)
|
||||
return alertAction
|
||||
|
|
|
@ -86,7 +86,11 @@ final class ProfileHeaderView: UIView {
|
|||
}
|
||||
|
||||
static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||
let avatarImageViewOverlayVisualEffectView = UIVisualEffectView(effect: nil)
|
||||
let avatarImageViewOverlayVisualEffectView: UIVisualEffectView = {
|
||||
let visualEffectView = UIVisualEffectView(effect: nil)
|
||||
visualEffectView.isUserInteractionEnabled = false
|
||||
return visualEffectView
|
||||
}()
|
||||
|
||||
let editAvatarBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
|
@ -343,10 +347,9 @@ extension ProfileHeaderView {
|
|||
nameMetaText.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
displayNameStackView.addSubview(nameMetaText.textView)
|
||||
NSLayoutConstraint.activate([
|
||||
nameMetaText.textView.topAnchor.constraint(equalTo: nameTextField.topAnchor),
|
||||
nameMetaText.textView.centerYAnchor.constraint(equalTo: nameTextField.centerYAnchor),
|
||||
nameMetaText.textView.leadingAnchor.constraint(equalTo: nameTextField.leadingAnchor),
|
||||
nameMetaText.textView.trailingAnchor.constraint(equalTo: nameTextField.trailingAnchor),
|
||||
nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextField.bottomAnchor),
|
||||
])
|
||||
|
||||
nameContainerStackView.addArrangedSubview(displayNameStackView)
|
||||
|
|
|
@ -103,6 +103,7 @@ final class SearchViewController: UIViewController, NeedsDependency {
|
|||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .singleLine
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
tableView.separatorColor = UIView.separatorColor
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
|
|
@ -8,19 +8,27 @@
|
|||
import func AVFoundation.AVMakeRect
|
||||
import UIKit
|
||||
import Combine
|
||||
import Nuke
|
||||
import FLAnimatedImage
|
||||
|
||||
final class ContextMenuImagePreviewViewController: UIViewController {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var viewModel: ContextMenuImagePreviewViewModel!
|
||||
|
||||
|
||||
var imageTask: ImageTask?
|
||||
let imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
let imageView = FLAnimatedImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.layer.masksToBounds = true
|
||||
return imageView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
imageTask?.cancel()
|
||||
imageTask = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -47,12 +55,13 @@ extension ContextMenuImagePreviewViewController {
|
|||
.sink { [weak self] url in
|
||||
guard let self = self else { return }
|
||||
guard let url = url else { return }
|
||||
self.imageView.af.setImage(
|
||||
withURL: url,
|
||||
placeholderImage: self.viewModel.thumbnail,
|
||||
imageTransition: .crossDissolve(0.2),
|
||||
runImageTransitionIfCached: true,
|
||||
completion: nil
|
||||
self.imageTask = Nuke.loadImage(
|
||||
with: url,
|
||||
options: ImageLoadingOptions(
|
||||
placeholder: self.viewModel.thumbnail,
|
||||
transition: .fadeIn(duration: 0.2)
|
||||
),
|
||||
into: self.imageView
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -95,7 +95,12 @@ final class StatusView: UIView {
|
|||
view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile
|
||||
return view
|
||||
}()
|
||||
let avatarImageView: UIImageView = FLAnimatedImageView()
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = FLAnimatedImageView()
|
||||
imageView.layer.shouldRasterize = true
|
||||
imageView.layer.rasterizationScale = UIScreen.main.scale
|
||||
return imageView
|
||||
}()
|
||||
let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton()
|
||||
|
||||
let nameLabel: ActiveLabel = {
|
||||
|
@ -217,6 +222,7 @@ final class StatusView: UIView {
|
|||
metaText.textView.textContainer.lineFragmentPadding = 0
|
||||
metaText.textView.textContainerInset = .zero
|
||||
metaText.textView.layer.masksToBounds = false
|
||||
|
||||
let paragraphStyle: NSMutableParagraphStyle = {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.lineSpacing = 5
|
||||
|
@ -234,7 +240,7 @@ final class StatusView: UIView {
|
|||
]
|
||||
return metaText
|
||||
}()
|
||||
|
||||
|
||||
private let headerInfoLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||
|
||||
var isRevealing = true
|
||||
|
@ -400,11 +406,6 @@ extension StatusView {
|
|||
statusContainerStackView.addArrangedSubview(contentMetaText.textView)
|
||||
contentMetaText.textView.setContentCompressionResistancePriority(.required - 1, for: .vertical)
|
||||
|
||||
// TODO:
|
||||
// link preview
|
||||
// statusContainerStackView.addArrangedSubview(linkPreview)
|
||||
// linkPreview.setContentHuggingPriority(.defaultHigh, for: .vertical)
|
||||
|
||||
// image
|
||||
statusContainerStackView.addArrangedSubview(statusMosaicImageViewContainer)
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ final class AvatarStackContainerButton: UIControl {
|
|||
static let maskOffset: CGFloat = 2
|
||||
|
||||
// UIControl.Event - Application: 0x0F000000
|
||||
static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000
|
||||
static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000
|
||||
var primaryActionState: UIControl.State = .normal
|
||||
|
||||
let topLeadingAvatarStackedImageView = AvatarStackedImageView()
|
||||
|
@ -46,6 +46,12 @@ final class AvatarStackContainerButton: UIControl {
|
|||
extension AvatarStackContainerButton {
|
||||
|
||||
private func _init() {
|
||||
topLeadingAvatarStackedImageView.layer.shouldRasterize = true
|
||||
topLeadingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale
|
||||
|
||||
bottomTrailingAvatarStackedImageView.layer.shouldRasterize = true
|
||||
bottomTrailingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale
|
||||
|
||||
topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(topLeadingAvatarStackedImageView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -74,6 +74,7 @@ final class StatusTableViewCell: UITableViewCell, StatusCell {
|
|||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
selectionStyle = .default
|
||||
statusView.statusMosaicImageViewContainer.reset()
|
||||
statusView.contentMetaText.textView.isSelectable = false
|
||||
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true
|
||||
|
@ -103,7 +104,12 @@ extension StatusTableViewCell {
|
|||
|
||||
private func _init() {
|
||||
backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||
|
||||
selectedBackgroundView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.Cell.highlight.color
|
||||
return view
|
||||
}()
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -23,7 +23,7 @@ struct MosaicImageViewModel {
|
|||
continue
|
||||
}
|
||||
let mosaicMeta = MosaicMeta(
|
||||
priviewURL: element.previewURL.flatMap { URL(string: $0) },
|
||||
previewURL: element.previewURL.flatMap { URL(string: $0) },
|
||||
url: url,
|
||||
size: CGSize(width: width, height: height),
|
||||
blurhash: element.blurhash,
|
||||
|
@ -39,7 +39,7 @@ struct MosaicImageViewModel {
|
|||
struct MosaicMeta {
|
||||
static let edgeMaxLength: CGFloat = 20
|
||||
|
||||
let priviewURL: URL?
|
||||
let previewURL: URL?
|
||||
let url: URL
|
||||
let size: CGSize
|
||||
let blurhash: String?
|
||||
|
|
|
@ -56,7 +56,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
toView.alpha = 0
|
||||
transitionContext.containerView.addSubview(toView)
|
||||
// set to image hidden
|
||||
toVC.pagingViewConttroller.view.alpha = 0
|
||||
toVC.pagingViewController.view.alpha = 0
|
||||
// set from image hidden. update hidden when paging. seealso: `MediaPreviewViewController`
|
||||
transitionItem.source.updateAppearance(position: .start, index: toVC.viewModel.currentPage.value)
|
||||
|
||||
|
@ -98,7 +98,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
toVC.pagingViewConttroller.view.alpha = 1
|
||||
toVC.pagingViewController.view.alpha = 1
|
||||
transitionImageView.removeFromSuperview()
|
||||
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) {
|
||||
toVC.closeButtonBackground.alpha = 1
|
||||
|
@ -112,8 +112,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
private func popTransition(using transitionContext: UIViewControllerContextTransitioning, curve: UIView.AnimationCurve = .easeInOut) -> UIViewPropertyAnimator {
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||
let fromView = transitionContext.view(forKey: .from),
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewConttroller.currentIndex else {
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewController.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewController.currentIndex else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
transitionItem.targetFrame = targetFrame
|
||||
|
||||
// disable interaction
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = false
|
||||
fromVC.pagingViewController.isUserInteractionEnabled = false
|
||||
|
||||
let animator = popInteractiveTransitionAnimator
|
||||
|
||||
|
@ -246,8 +246,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController,
|
||||
let _ = transitionContext.view(forKey: .from),
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewConttroller.currentIndex else {
|
||||
let mediaPreviewImageViewController = fromVC.pagingViewController.currentViewController as? MediaPreviewImageViewController,
|
||||
let index = fromVC.pagingViewController.currentIndex else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
transitionItem.targetFrame = targetFrame ?? snapshot.frame
|
||||
|
||||
// disable interaction
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = false
|
||||
fromVC.pagingViewController.isUserInteractionEnabled = false
|
||||
|
||||
let animator = popInteractiveTransitionAnimator
|
||||
|
||||
|
@ -315,7 +315,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
|
|||
}
|
||||
|
||||
animator.addCompletion { position in
|
||||
fromVC.pagingViewConttroller.isUserInteractionEnabled = true
|
||||
fromVC.pagingViewController.isUserInteractionEnabled = true
|
||||
fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1
|
||||
self.transitionItem.imageView?.isHidden = position == .end
|
||||
self.transitionItem.snapshotRaw?.alpha = position == .start ? 1.0 : 0.0
|
||||
|
|
|
@ -56,7 +56,7 @@ extension MediaPreviewTransitionController: UIGestureRecognizerDelegate {
|
|||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === panGestureRecognizer {
|
||||
guard let mediaPreviewViewController = self.mediaPreviewViewController else { return false }
|
||||
return mediaPreviewViewController.isInteractiveDismissable()
|
||||
return mediaPreviewViewController.isInteractiveDismissible()
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
import Foundation
|
||||
|
||||
protocol MediaPreviewingViewController: AnyObject {
|
||||
func isInteractiveDismissable() -> Bool
|
||||
func isInteractiveDismissible() -> Bool
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import os.log
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import Kingfisher
|
||||
import MastodonSDK
|
||||
|
||||
extension MastodonAttachmentService {
|
||||
|
|
|
@ -9,7 +9,6 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import PhotosUI
|
||||
import Kingfisher
|
||||
import GameplayKit
|
||||
import MobileCoreServices
|
||||
import MastodonSDK
|
||||
|
|
|
@ -11,7 +11,6 @@ import Combine
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
import AlamofireImage
|
||||
import Kingfisher
|
||||
|
||||
class AppContext: ObservableObject {
|
||||
|
||||
|
@ -124,7 +123,6 @@ extension AppContext {
|
|||
func purgeCache() -> AnyPublisher<ByteCount, Never> {
|
||||
Publishers.MergeMany([
|
||||
AppContext.purgeAlamofireImageCache(),
|
||||
AppContext.purgeKingfisherCache(),
|
||||
AppContext.purgeTemporaryDirectory(),
|
||||
])
|
||||
.reduce(0, +)
|
||||
|
@ -146,29 +144,6 @@ extension AppContext {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private static func purgeKingfisherCache() -> AnyPublisher<ByteCount, Never> {
|
||||
Future<ByteCount, Never> { promise in
|
||||
KingfisherManager.shared.cache.calculateDiskStorageSize { result in
|
||||
switch result {
|
||||
case .success(let diskBytes):
|
||||
KingfisherManager.shared.cache.clearCache()
|
||||
KingfisherManager.shared.cache.calculateDiskStorageSize { currentResult in
|
||||
switch currentResult {
|
||||
case .success(let currentDiskBytes):
|
||||
let purgedDiskBytes = max(0, Int(diskBytes) - Int(currentDiskBytes))
|
||||
promise(.success(purgedDiskBytes))
|
||||
case .failure:
|
||||
promise(.success(0))
|
||||
}
|
||||
}
|
||||
case .failure:
|
||||
promise(.success(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private static func purgeTemporaryDirectory() -> AnyPublisher<ByteCount, Never> {
|
||||
Future<ByteCount, Never> { promise in
|
||||
AppContext.purgeCacheWorkingQueue.async {
|
||||
|
|
|
@ -56,6 +56,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppDelegate {
|
||||
|
|
Loading…
Reference in New Issue