From c7edf45164cefbcb378fc6a4499a1342716e28ca Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 14:03:30 +0800 Subject: [PATCH 01/12] fix: add spacing between status header icon and text --- Mastodon/Diffiable/Section/StatusSection.swift | 2 +- Mastodon/Scene/Share/View/Content/StatusView.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index edf60c85b..4b4005227 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -305,7 +305,7 @@ extension StatusSection { switch result { case .failure: break - case .success(let response) + case .success: statusItemAttribute.isImageLoaded.value = true } } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 337f07f04..d61210117 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -253,6 +253,7 @@ extension StatusView { // header container: [icon | info] let headerContainerStackView = UIStackView() headerContainerStackView.axis = .horizontal + headerContainerStackView.spacing = 4 headerContainerStackView.addArrangedSubview(headerIconLabel) headerContainerStackView.addArrangedSubview(headerInfoLabel) headerIconLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) From cd31d793574216251e7466056a19df3100e89db2 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 14:19:43 +0800 Subject: [PATCH 02/12] fix: add missing "ago" for timestamp --- Localization/app.json | 3 ++- Mastodon/Extension/Date.swift | 3 ++- Mastodon/Generated/Strings.swift | 4 ++++ Mastodon/Resources/ar.lproj/Localizable.strings | 1 + Mastodon/Resources/en.lproj/Localizable.strings | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 3e90f799f..90a63fc18 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -174,7 +174,8 @@ }, "timeline": { "timestamp": { - "now": "Now" + "now": "Now", + "time_ago": "%s ago" }, "loader": { "load_missing_posts": "Load missing posts", diff --git a/Mastodon/Extension/Date.swift b/Mastodon/Extension/Date.swift index c1d73a438..daa5eab6c 100644 --- a/Mastodon/Extension/Date.swift +++ b/Mastodon/Extension/Date.swift @@ -22,7 +22,8 @@ extension Date { if earlierDate.timeIntervalSince(latest) >= -60 { return L10n.Common.Controls.Timeline.Timestamp.now } else { - return latest.shortTimeAgo(since: earlierDate) + let interval = latest.shortTimeAgo(since: earlierDate) // 1s + return L10n.Common.Controls.Timeline.Timestamp.timeAgo(interval) // 1s ago } } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 34556e8a6..072105193 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -383,6 +383,10 @@ internal enum L10n { internal enum Timestamp { /// Now internal static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now") + /// %@ ago + internal static func timeAgo(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.TimeAgo", String(describing: p1)) + } } } } diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index a95c2cbce..3d976d72a 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -133,6 +133,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; "Common.Controls.Timeline.Timestamp.Now" = "Now"; +"Common.Controls.Timeline.Timestamp.TimeAgo" = "%@ ago"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index a95c2cbce..3d976d72a 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -133,6 +133,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; "Common.Controls.Timeline.Timestamp.Now" = "Now"; +"Common.Controls.Timeline.Timestamp.TimeAgo" = "%@ ago"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; From 41fa6f2b3eb87f8466187758ce321d1ae7347225 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 14:30:24 +0800 Subject: [PATCH 03/12] fix: set barButtonItem and tabBarItem tint color to brand blue --- .../HashtagTimeline/HashtagTimelineViewController.swift | 2 +- .../Scene/HomeTimeline/HomeTimelineViewController.swift | 4 ++-- Mastodon/Scene/MainTab/MainTabBarController.swift | 8 +++++--- Mastodon/Supporting Files/AppDelegate.swift | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 172842102..7c7e8a274 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -24,7 +24,7 @@ class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPre let composeBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.Label.highlight.color + barButtonItem.tintColor = Asset.Colors.brandBlue.color barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index bb6a651bc..b293c7e59 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -38,14 +38,14 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media let settingBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.Label.highlight.color + barButtonItem.tintColor = Asset.Colors.brandBlue.color barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() let composeBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.Label.highlight.color + barButtonItem.tintColor = Asset.Colors.brandBlue.color barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index 2df0cdf31..cd56e08c5 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -106,9 +106,11 @@ extension MainTabBarController { selectedIndex = 0 // TODO: custom accent color -// let tabBarAppearance = UITabBarAppearance() -// tabBarAppearance.configureWithDefaultBackground() -// tabBar.standardAppearance = tabBarAppearance + let tabBarAppearance = UITabBarAppearance() + tabBarAppearance.configureWithDefaultBackground() + tabBarAppearance.selectionIndicatorTintColor = Asset.Colors.brandBlue.color + tabBar.standardAppearance = tabBarAppearance + context.apiService.error .receive(on: DispatchQueue.main) diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 6c49638da..9b0846499 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -23,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion") UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle") + // Setup notification UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() From d9e7052cffcde86943a91d17dbf7d590e728f606 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 14:33:54 +0800 Subject: [PATCH 04/12] fix: update status action icon tint color --- .../Button/action.toolbar.colorset/Contents.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Button/action.toolbar.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Button/action.toolbar.colorset/Contents.json index 8c938d914..8b7864ebe 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/Button/action.toolbar.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/Button/action.toolbar.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.600", - "blue" : "0", - "green" : "0", - "red" : "0" + "blue" : "67", + "green" : "60", + "red" : "60" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.600", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "245", + "green" : "235", + "red" : "235" } }, "idiom" : "universal" From 931197e51c0a308697b67c5252df0531c54c3a5b Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 16:31:34 +0800 Subject: [PATCH 05/12] chore: profile status header and name label configure --- Mastodon.xcodeproj/project.pbxproj | 4 + .../xcschemes/xcschememanagement.plist | 6 +- .../Diffiable/Section/StatusSection.swift | 41 ++++++++-- Mastodon/Extension/ActiveLabel.swift | 9 +++ Mastodon/Helper/MastodonStatusContent.swift | 29 ++++++- ...der+UITableViewDataSourcePrefetching.swift | 25 +++--- .../HomeTimelineViewController.swift | 18 ++++- .../Service/StatusContentCacheService.swift | 77 +++++++++++++++++++ Mastodon/State/AppContext.swift | 1 + Mastodon/Supporting Files/AppDelegate.swift | 8 ++ Podfile | 1 + Podfile.lock | 6 +- 12 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 Mastodon/Service/StatusContentCacheService.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f423a5290..36750ea47 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -417,6 +417,7 @@ DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; }; DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; }; + DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; @@ -989,6 +990,7 @@ DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = ""; }; + DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = ""; }; DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; @@ -1358,6 +1360,7 @@ DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */, DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, + DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */, ); path = Service; sourceTree = ""; @@ -2985,6 +2988,7 @@ DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */, DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, + DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index e8494ef81..2fb2d9806 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,12 +12,12 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 15 + 17 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 2 + 1 Mastodon - Release.xcscheme_^#shared#^_ @@ -27,7 +27,7 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 12 + 2 NotificationService.xcscheme_^#shared#^_ diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 4b4005227..b0a4ffbcb 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -194,9 +194,15 @@ extension StatusSection { let author = (status.reblog ?? status).author return author.displayName.isEmpty ? author.username : author.displayName }() - cell.statusView.nameLabel.configure(content: nameText, emojiDict: (status.reblog ?? status).author.emojiDict) + MastodonStatusContent.parseResult(content: nameText, emojiDict: (status.reblog ?? status).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.usernameLabel.text = "@" + (status.reblog ?? status).author.acct - + // set avatar if let reblog = status.reblog { cell.statusView.avatarButton.isHidden = true @@ -210,6 +216,19 @@ extension StatusSection { } // set text +// func configureStatusContent() { +// let content = (status.reblog ?? status).content +// let emojiDict = (status.reblog ?? status).emojiDict +// if let cachedParseResult = AppContext.shared.statusContentCacheService.parseResult(content: content, emojiDict: emojiDict) { +// cell.statusView.activeTextLabel.configure(contentParseResult: cachedParseResult) +// } else { +// cell.statusView.activeTextLabel.configure( +// content: (status.reblog ?? status).content, +// emojiDict: (status.reblog ?? status).emojiDict +// ) +// } +// } +// configureStatusContent() cell.statusView.activeTextLabel.configure( content: (status.reblog ?? status).content, emojiDict: (status.reblog ?? status).emojiDict @@ -221,7 +240,7 @@ extension StatusSection { cell.statusView.updateVisibility(visibility: visibility) cell.statusView.revealContentWarningButton.publisher(for: \.isHidden) - .receive(on: RunLoop.main) + .receive(on: DispatchQueue.main) .sink { [weak cell] isHidden in cell?.statusView.visibilityImageView.isHidden = !isHidden } @@ -646,7 +665,13 @@ extension StatusSection { let name = author.displayName.isEmpty ? author.username : author.displayName return L10n.Common.Controls.Status.userReblogged(name) }() - cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.author.emojiDict) + MastodonStatusContent.parseResult(content: headerText, emojiDict: status.author.emojiDict) + .receive(on: DispatchQueue.main) + .sink { [weak cell] parseResult in + guard let cell = cell else { return } + cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult) + } + .store(in: &cell.disposeBag) cell.statusView.headerInfoLabel.isAccessibilityElement = true } else if status.inReplyToID != nil { cell.statusView.headerContainerView.isHidden = false @@ -659,7 +684,13 @@ extension StatusSection { let name = author.displayName.isEmpty ? author.username : author.displayName return L10n.Common.Controls.Status.userRepliedTo(name) }() - cell.statusView.headerInfoLabel.configure(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:]) + MastodonStatusContent.parseResult(content: headerText, emojiDict: status.replyTo?.author.emojiDict ?? [:]) + .receive(on: DispatchQueue.main) + .sink { [weak cell] parseResult in + guard let cell = cell else { return } + cell.statusView.headerInfoLabel.configure(contentParseResult: parseResult) + } + .store(in: &cell.disposeBag) cell.statusView.headerInfoLabel.isAccessibilityElement = true } else { cell.statusView.headerContainerView.isHidden = true diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 5d2ca7c63..3d59fc92d 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -61,6 +61,7 @@ extension ActiveLabel { } extension ActiveLabel { + /// status content func configure(content: String, emojiDict: MastodonStatusContent.EmojiDict) { attributedText = nil @@ -76,6 +77,14 @@ extension ActiveLabel { } } + func configure(contentParseResult parseResult: MastodonStatusContent.ParseResult?) { + attributedText = nil + activeEntities.removeAll() + text = parseResult?.trimmed ?? "" + activeEntities = parseResult?.activeEntities ?? [] + accessibilityLabel = parseResult?.original ?? nil + } + /// account note func configure(note: String, emojiDict: MastodonStatusContent.EmojiDict) { configure(content: note, emojiDict: emojiDict) diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index d338615df..dbeb26473 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine import Kanna import ActiveLabel @@ -14,6 +15,18 @@ enum MastodonStatusContent { typealias EmojiShortcode = String typealias EmojiDict = [EmojiShortcode: URL] + static let workingQueue = DispatchQueue(label: "org.joinmastodon.app.ActiveLabel.working-queue", qos: .userInteractive, attributes: .concurrent) + + static func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> AnyPublisher { + return Future { promise in + self.workingQueue.async { + let parseResult = try? MastodonStatusContent.parse(content: content, emojiDict: emojiDict) + promise(.success(parseResult)) + } + } + .eraseToAnyPublisher() + } + static func parse(content: String, emojiDict: EmojiDict) throws -> MastodonStatusContent.ParseResult { let document: String = { var content = content @@ -113,11 +126,25 @@ extension String { } extension MastodonStatusContent { - struct ParseResult { + struct ParseResult: Hashable { let document: String let original: String let trimmed: String let activeEntities: [ActiveEntity] + + static func == (lhs: MastodonStatusContent.ParseResult, rhs: MastodonStatusContent.ParseResult) -> Bool { + return lhs.document == rhs.document + && lhs.original == rhs.original + && lhs.trimmed == rhs.trimmed + && lhs.activeEntities.count == rhs.activeEntities.count // FIXME: + } + + func hash(into hasher: inout Hasher) { + hasher.combine(document) + hasher.combine(original) + hasher.combine(trimmed) + hasher.combine(activeEntities.count) // FIXME: + } } } diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift index a0aaa543e..a9e1898a0 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDataSourcePrefetching.swift @@ -33,17 +33,22 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard let self = self else { return } for objectID in statusObjectIDs { let status = backgroundManagedObjectContext.object(with: objectID) as! Status - guard let replyToID = status.inReplyToID, status.replyTo == nil else { - // skip - continue + + // fetch in-reply info if needs + if let replyToID = status.inReplyToID, status.replyTo == nil { + self.context.statusPrefetchingService.prefetchReplyTo( + domain: domain, + statusObjectID: status.objectID, + statusID: status.id, + replyToStatusID: replyToID, + authorizationBox: activeMastodonAuthenticationBox + ) } - self.context.statusPrefetchingService.prefetchReplyTo( - domain: domain, - statusObjectID: status.objectID, - statusID: status.id, - replyToStatusID: replyToID, - authorizationBox: activeMastodonAuthenticationBox - ) + +// self.context.statusContentCacheService.prefetch( +// content: (status.reblog ?? status).content, +// emojiDict: (status.reblog ?? status).emojiDict +// ) } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index b293c7e59..6aed1f574 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -15,6 +15,10 @@ import GameplayKit import MastodonSDK import AlamofireImage +#if DEBUG +import GDPerformanceView_Swift +#endif + final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -87,7 +91,9 @@ extension HomeTimelineViewController { titleView.delegate = self viewModel.homeTimelineNavigationBarTitleViewModel.state - .receive(on: DispatchQueue.main) + .removeDuplicates() + .debounce(for: 0.3, scheduler: RunLoop.main) + .receive(on: RunLoop.main) .sink { [weak self] state in guard let self = self else { return } self.titleView.configure(state: state) @@ -98,6 +104,8 @@ extension HomeTimelineViewController { #if DEBUG // long press to trigger debug menu settingBarButtonItem.menu = debugMenu + PerformanceMonitor.shared().delegate = self + #else settingBarButtonItem.target = self settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) @@ -554,3 +562,11 @@ extension HomeTimelineViewController: StatusTableViewControllerNavigateable { statusKeyCommandHandler(sender) } } + +#if DEBUG +extension HomeTimelineViewController: PerformanceMonitorDelegate { + func performanceMonitor(didReport performanceReport: PerformanceReport) { + // print(performanceReport) + } +} +#endif diff --git a/Mastodon/Service/StatusContentCacheService.swift b/Mastodon/Service/StatusContentCacheService.swift new file mode 100644 index 000000000..ff17b0478 --- /dev/null +++ b/Mastodon/Service/StatusContentCacheService.swift @@ -0,0 +1,77 @@ +// +// StatusContentCacheService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-17. +// + +import UIKit +import Combine + +final class StatusContentCacheService { + + var disposeBag = Set() + + let cache = NSCache() + + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.BlurhashImageCacheService.working-queue", qos: .userInitiated, attributes: .concurrent) + + func parseResult(content: String, emojiDict: MastodonStatusContent.EmojiDict) -> MastodonStatusContent.ParseResult? { + let key = Key(content: content, emojiDict: emojiDict) + return cache.object(forKey: key)?.parseResult + } + + func prefetch(content: String, emojiDict: MastodonStatusContent.EmojiDict) { + let key = Key(content: content, emojiDict: emojiDict) + guard cache.object(forKey: key) == nil else { return } + MastodonStatusContent.parseResult(content: content, emojiDict: emojiDict) + .sink { [weak self] parseResult in + guard let self = self else { return } + guard let parseResult = parseResult else { return } + let wrapper = ParseResultWrapper(parseResult: parseResult) + self.cache.setObject(wrapper, forKey: key) + } + .store(in: &disposeBag) + } + +} + +extension StatusContentCacheService { + class Key: NSObject { + let content: String + let emojiDict: MastodonStatusContent.EmojiDict + + init(content: String, emojiDict: MastodonStatusContent.EmojiDict) { + self.content = content + self.emojiDict = emojiDict + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? Key else { return false } + return object.content == content + && object.emojiDict == emojiDict + } + + override var hash: Int { + return content.hashValue ^ + emojiDict.hashValue + } + } + + class ParseResultWrapper: NSObject { + let parseResult: MastodonStatusContent.ParseResult + + init(parseResult: MastodonStatusContent.ParseResult) { + self.parseResult = parseResult + } + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? ParseResultWrapper else { return false } + return object.parseResult == parseResult + } + + override var hash: Int { + return parseResult.hashValue + } + } +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index e9b6073aa..b6b0cdb55 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -38,6 +38,7 @@ class AppContext: ObservableObject { let placeholderImageCacheService = PlaceholderImageCacheService() let blurhashImageCacheService = BlurhashImageCacheService() + let statusContentCacheService = StatusContentCacheService() let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 9b0846499..e6ccaaac0 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -10,6 +10,10 @@ import UIKit import UserNotifications import AppShared +#if DEBUG +import GDPerformanceView_Swift +#endif + @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -27,6 +31,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UNUserNotificationCenter.current().delegate = self application.registerForRemoteNotifications() + #if DEBUG + PerformanceMonitor.shared().start() + #endif + return true } diff --git a/Podfile b/Podfile index d9f295c29..796473d68 100644 --- a/Podfile +++ b/Podfile @@ -16,6 +16,7 @@ target 'Mastodon' do # DEBUG pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] + pod 'GDPerformanceView-Swift', '~> 2.1.1', :configurations => ['Debug'] target 'MastodonTests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index ea8ecc821..4e7baf347 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,7 @@ PODS: - DateToolsSwift (5.0.0) - FLEX (4.4.1) + - GDPerformanceView-Swift (2.1.1) - Kanna (5.2.4) - Keys (1.0.1) - SwiftGen (6.4.0) @@ -9,6 +10,7 @@ PODS: DEPENDENCIES: - DateToolsSwift (~> 5.0.0) - FLEX (~> 4.4.0) + - GDPerformanceView-Swift (~> 2.1.1) - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) @@ -18,6 +20,7 @@ SPEC REPOS: trunk: - DateToolsSwift - FLEX + - GDPerformanceView-Swift - Kanna - SwiftGen - "UITextField+Shake" @@ -29,11 +32,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab + GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77 Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa +PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd COCOAPODS: 1.10.1 From 04dbe9ebc9f947cd874036c57e924c6233ceb88f Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 16:44:57 +0800 Subject: [PATCH 06/12] fix: reblog and favorite count update delay issue --- .../Protocol/StatusProvider/StatusProviderFacade.swift | 2 +- Mastodon/Service/APIService/APIService+Favorite.swift | 8 ++++++++ Mastodon/Service/APIService/APIService+Reblog.swift | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 98fecc975..a21422159 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -247,7 +247,7 @@ extension StatusProviderFacade { } .map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in return context.apiService.favorite( - statusObjectID: statusObjectID, + statusObjectID: statusObjectID, mastodonUserObjectID: mastodonUserObjectID, favoriteKind: favoriteKind ) diff --git a/Mastodon/Service/APIService/APIService+Favorite.swift b/Mastodon/Service/APIService/APIService+Favorite.swift index 23206494e..d3f1d81ec 100644 --- a/Mastodon/Service/APIService/APIService+Favorite.swift +++ b/Mastodon/Service/APIService/APIService+Favorite.swift @@ -30,6 +30,14 @@ extension APIService { let targetStatusID = targetStatus.id _targetStatusID = targetStatusID + let favouritesCount: NSNumber + switch favoriteKind { + case .create: + favouritesCount = NSNumber(value: targetStatus.favouritesCount.intValue + 1) + case .destroy: + favouritesCount = NSNumber(value: max(0, targetStatus.favouritesCount.intValue - 1)) + } + targetStatus.update(favouritesCount: favouritesCount) targetStatus.update(liked: favoriteKind == .create, by: mastodonUser) } diff --git a/Mastodon/Service/APIService/APIService+Reblog.swift b/Mastodon/Service/APIService/APIService+Reblog.swift index 6dc9f189b..adfac3066 100644 --- a/Mastodon/Service/APIService/APIService+Reblog.swift +++ b/Mastodon/Service/APIService/APIService+Reblog.swift @@ -29,12 +29,17 @@ extension APIService { let targetStatusID = targetStatus.id _targetStatusID = targetStatusID + let reblogsCount: NSNumber switch reblogKind { case .reblog: targetStatus.update(reblogged: true, by: mastodonUser) + reblogsCount = NSNumber(value: targetStatus.reblogsCount.intValue + 1) case .undoReblog: targetStatus.update(reblogged: false, by: mastodonUser) + reblogsCount = NSNumber(value: max(0, targetStatus.reblogsCount.intValue - 1)) } + + targetStatus.update(reblogsCount: reblogsCount) } .tryMap { result in From 7e1571a4932ce9a46c685bf52ab22784647f1419 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 18:21:18 +0800 Subject: [PATCH 07/12] fix: action buttons for status missing highlighed state --- .../Share/View/ToolBar/ActionToolBarContainer.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index f10e55941..794f9c761 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -17,10 +17,10 @@ protocol ActionToolbarContainerDelegate: AnyObject { final class ActionToolbarContainer: UIView { - let replyButton = HitTestExpandedButton() - let reblogButton = HitTestExpandedButton() - let favoriteButton = HitTestExpandedButton() - let moreButton = HitTestExpandedButton() + let replyButton = HighlightDimmableButton() + let reblogButton = HighlightDimmableButton() + let favoriteButton = HighlightDimmableButton() + let moreButton = HighlightDimmableButton() var isReblogButtonHighlight: Bool = false { didSet { isReblogButtonHighlightStateDidChange(to: isReblogButtonHighlight) } @@ -97,6 +97,7 @@ extension ActionToolbarContainer { button.titleLabel?.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular) button.setTitle("", for: .normal) button.setTitleColor(.secondaryLabel, for: .normal) + button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding) } From b52508dd03e95e59066147f5f102cd9e7cc01b13 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 18:21:49 +0800 Subject: [PATCH 08/12] fix: slider not smooth for audio playback issue --- .../View/Container/AudioContainerView.swift | 1 + .../ViewModel/AudioContainerViewModel.swift | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift index 336fade8f..49bad532a 100644 --- a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift @@ -52,6 +52,7 @@ final class AudioContainerView: UIView { let slider: UISlider = { let slider = UISlider() + slider.isContinuous = true slider.translatesAutoresizingMaskIntoConstraints = false slider.minimumTrackTintColor = Asset.Colors.Slider.bar.color slider.maximumTrackTintColor = Asset.Colors.Slider.bar.color diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index 2bc6db226..1176c97d5 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -10,6 +10,7 @@ import Foundation import UIKit class AudioContainerViewModel { + static func configure( cell: StatusCell, audioAttachment: Attachment, @@ -36,11 +37,12 @@ class AudioContainerViewModel { } } .store(in: &cell.disposeBag) + audioView.slider.maximumValue = Float(duration) audioView.slider.publisher(for: .valueChanged) .sink { [weak audioService] slider in guard let audioService = audioService else { return } let slider = slider as! UISlider - let time = Double(slider.value) * duration + let time = TimeInterval(slider.value) audioService.seekToTime(time: time) } .store(in: &cell.disposeBag) @@ -58,24 +60,24 @@ class AudioContainerViewModel { let audioView = cell.statusView.audioView var lastCurrentTimeSubject: TimeInterval? audioService.currentTimeSubject - .throttle(for: 0.33, scheduler: DispatchQueue.main, latest: true) - .compactMap { [weak audioService] time -> (TimeInterval, Float)? in + .throttle(for: 0.008, scheduler: DispatchQueue.main, latest: true) + .compactMap { [weak audioService] time -> TimeInterval? in defer { lastCurrentTimeSubject = time } guard audioAttachment === audioService?.attachment else { return nil } - guard let duration = audioAttachment.meta?.original?.duration else { return nil } + // guard let duration = audioAttachment.meta?.original?.duration else { return nil } if let lastCurrentTimeSubject = lastCurrentTimeSubject, time != 0.0 { guard abs(time - lastCurrentTimeSubject) < 0.5 else { return nil } // debounce } guard !audioView.slider.isTracking else { return nil } - return (time, Float(time / duration)) + return TimeInterval(time) } - .sink(receiveValue: { time, progress in + .sink(receiveValue: { time in audioView.timeLabel.text = time.asString(style: .positional) - audioView.slider.setValue(progress, animated: true) + audioView.slider.setValue(Float(time), animated: true) }) .store(in: &cell.disposeBag) audioService.playbackState From 41322c63e372721cad0fe2b2b61b008f4e8d8a05 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 18:31:14 +0800 Subject: [PATCH 09/12] fix: update audio player slider appearance --- Mastodon/Generated/Assets.swift | 2 +- .../Colors/Slider/bar.colorset/Contents.json | 20 ---------- .../Slider/track.colorset/Contents.json | 38 +++++++++++++++++++ .../View/Container/AudioContainerView.swift | 4 +- .../ViewModel/AudioContainerViewModel.swift | 6 +-- 5 files changed, 44 insertions(+), 26 deletions(-) delete mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Slider/bar.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Colors/Slider/track.colorset/Contents.json diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 1ea9e8d6a..b71299203 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -81,7 +81,7 @@ internal enum Asset { internal static let searchCard = ColorAsset(name: "Colors/Shadow/SearchCard") } internal enum Slider { - internal static let bar = ColorAsset(name: "Colors/Slider/bar") + internal static let track = ColorAsset(name: "Colors/Slider/track") } internal enum TextField { internal static let background = ColorAsset(name: "Colors/TextField/background") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Slider/bar.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Slider/bar.colorset/Contents.json deleted file mode 100644 index dc91052f7..000000000 --- a/Mastodon/Resources/Assets.xcassets/Colors/Slider/bar.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "147", - "green" : "106", - "red" : "51" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/Slider/track.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/Slider/track.colorset/Contents.json new file mode 100644 index 000000000..1c84ad477 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/Slider/track.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "213", + "green" : "213", + "red" : "212" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift index 49bad532a..5661f833a 100644 --- a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift @@ -54,8 +54,8 @@ final class AudioContainerView: UIView { let slider = UISlider() slider.isContinuous = true slider.translatesAutoresizingMaskIntoConstraints = false - slider.minimumTrackTintColor = Asset.Colors.Slider.bar.color - slider.maximumTrackTintColor = Asset.Colors.Slider.bar.color + slider.minimumTrackTintColor = Asset.Colors.Slider.track.color + slider.maximumTrackTintColor = Asset.Colors.Slider.track.color if let image = UIImage.placeholder(size: CGSize(width: 22, height: 22), color: .white).withRoundedCorners(radius: 11) { slider.setThumbImage(image, for: .normal) } diff --git a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift index 1176c97d5..c31802211 100644 --- a/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/AudioContainerViewModel.swift @@ -100,14 +100,14 @@ class AudioContainerViewModel { switch playbackState { case .stopped: audioView.playButton.isSelected = false - audioView.slider.isEnabled = false + audioView.slider.isUserInteractionEnabled = false audioView.slider.setValue(0, animated: false) case .paused: audioView.playButton.isSelected = false - audioView.slider.isEnabled = true + audioView.slider.isUserInteractionEnabled = true case .playing, .readyToPlay: audioView.playButton.isSelected = true - audioView.slider.isEnabled = true + audioView.slider.isUserInteractionEnabled = true default: assertionFailure() } From a1b553a05d3d376d4579d3fd944afed7e6230de9 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 18:43:06 +0800 Subject: [PATCH 10/12] fix: hide media indicator for video --- .../Diffiable/Section/StatusSection.swift | 22 +++---------- ...ContainerView+MediaTypeIndicotorView.swift | 27 ++++------------ .../View/Container/PlayerContainerView.swift | 32 ++++++------------- 3 files changed, 21 insertions(+), 60 deletions(-) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index b0a4ffbcb..8741ff973 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -379,8 +379,7 @@ extension StatusSection { }() if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, - let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) - { + let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) { var parent: UIViewController? var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil switch cell { @@ -408,22 +407,11 @@ extension StatusSection { playerViewController.player = videoPlayerViewModel.player playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) - if videoPlayerViewModel.videoKind == .gif { + switch videoPlayerViewModel.videoKind { + case .gif: playerContainerView.setMediaIndicator(isHidden: false) - } else { - videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in - UIView.animate(withDuration: 0.33) { - switch timeControlStatus { - case .playing: - playerContainerView.setMediaIndicator(isHidden: true) - case .paused, .waitingToPlayAtSpecifiedRate: - playerContainerView.setMediaIndicator(isHidden: false) - @unknown default: - assertionFailure() - } - } - } - .store(in: &cell.disposeBag) + case .video: + playerContainerView.setMediaIndicator(isHidden: true) } playerContainerView.isHidden = false diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift index 12f822986..3210fadf7 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView+MediaTypeIndicotorView.swift @@ -9,7 +9,7 @@ import UIKit extension PlayerContainerView { - final class MediaTypeIndicotorView: UIView { + final class MediaTypeIndicatorView: UIView { static let indicatorViewSize = CGSize(width: 47, height: 25) @@ -60,7 +60,7 @@ extension PlayerContainerView { } -extension PlayerContainerView.MediaTypeIndicotorView { +extension PlayerContainerView.MediaTypeIndicatorView { private func _init() { backgroundColor = Asset.Colors.Background.mediaTypeIndicotor.color @@ -87,14 +87,10 @@ extension PlayerContainerView.MediaTypeIndicotorView { switch kind { case .gif: - label.font = PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .heavy, fontSize: fontSize) + label.font = PlayerContainerView.MediaTypeIndicatorView.roundedFont(weight: .heavy, fontSize: fontSize) label.text = "GIF" case .video: - let configuration = UIImage.SymbolConfiguration(font: PlayerContainerView.MediaTypeIndicotorView.roundedFont(weight: .regular, fontSize: fontSize)) - let image = UIImage(systemName: "video.fill", withConfiguration: configuration)! - let attachment = NSTextAttachment() - attachment.image = image.withTintColor(.white) - label.attributedText = NSAttributedString(attachment: attachment) + label.text = " " } } @@ -103,12 +99,12 @@ extension PlayerContainerView.MediaTypeIndicotorView { #if canImport(SwiftUI) && DEBUG import SwiftUI -struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider { +struct PlayerContainerViewMediaTypeIndicatorView_Previews: PreviewProvider { static var previews: some View { Group { UIViewPreview(width: 47) { - let view = PlayerContainerView.MediaTypeIndicotorView() + let view = PlayerContainerView.MediaTypeIndicatorView() view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.heightAnchor.constraint(equalToConstant: 25), @@ -118,17 +114,6 @@ struct PlayerContainerViewMediaTypeIndicotorView_Previews: PreviewProvider { return view } .previewLayout(.fixed(width: 47, height: 25)) - UIViewPreview(width: 47) { - let view = PlayerContainerView.MediaTypeIndicotorView() - view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - view.heightAnchor.constraint(equalToConstant: 25), - view.widthAnchor.constraint(equalToConstant: 47), - ]) - view.setMediaKind(kind: .video) - return view - } - .previewLayout(.fixed(width: 47, height: 25)) } } diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index 32ee48df9..1d33572a2 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -28,8 +28,7 @@ final class PlayerContainerView: UIView { let playerViewController = AVPlayerViewController() - let mediaTypeIndicotorView = MediaTypeIndicotorView() - let mediaTypeIndicotorViewInContentWarningOverlay = MediaTypeIndicotorView() + let mediaTypeIndicatorView = MediaTypeIndicatorView() weak var delegate: PlayerContainerViewDelegate? @@ -66,22 +65,13 @@ extension PlayerContainerView { playerViewController.view.layer.cornerCurve = .continuous // mediaType - mediaTypeIndicotorView.translatesAutoresizingMaskIntoConstraints = false - playerViewController.contentOverlayView!.addSubview(mediaTypeIndicotorView) + mediaTypeIndicatorView.translatesAutoresizingMaskIntoConstraints = false + playerViewController.contentOverlayView!.addSubview(mediaTypeIndicatorView) NSLayoutConstraint.activate([ - mediaTypeIndicotorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), - mediaTypeIndicotorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor), - mediaTypeIndicotorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1), - mediaTypeIndicotorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1), - ]) - - mediaTypeIndicotorViewInContentWarningOverlay.translatesAutoresizingMaskIntoConstraints = false - contentWarningOverlayView.addSubview(mediaTypeIndicotorViewInContentWarningOverlay) - NSLayoutConstraint.activate([ - mediaTypeIndicotorViewInContentWarningOverlay.bottomAnchor.constraint(equalTo: contentWarningOverlayView.bottomAnchor), - mediaTypeIndicotorViewInContentWarningOverlay.rightAnchor.constraint(equalTo: contentWarningOverlayView.rightAnchor), - mediaTypeIndicotorViewInContentWarningOverlay.heightAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.height).priority(.required - 1), - mediaTypeIndicotorViewInContentWarningOverlay.widthAnchor.constraint(equalToConstant: MediaTypeIndicotorView.indicatorViewSize.width).priority(.required - 1), + mediaTypeIndicatorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), + mediaTypeIndicatorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor), + mediaTypeIndicatorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.height).priority(.required - 1), + mediaTypeIndicatorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.width).priority(.required - 1), ]) contentWarningOverlayView.delegate = self @@ -149,19 +139,17 @@ extension PlayerContainerView { contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor) ]) - bringSubviewToFront(mediaTypeIndicotorView) + bringSubviewToFront(mediaTypeIndicatorView) return playerViewController } func setMediaKind(kind: VideoPlayerViewModel.Kind) { - mediaTypeIndicotorView.setMediaKind(kind: kind) - mediaTypeIndicotorViewInContentWarningOverlay.setMediaKind(kind: kind) + mediaTypeIndicatorView.setMediaKind(kind: kind) } func setMediaIndicator(isHidden: Bool) { - mediaTypeIndicotorView.alpha = isHidden ? 0 : 1 - mediaTypeIndicotorViewInContentWarningOverlay.alpha = isHidden ? 0 : 1 + mediaTypeIndicatorView.alpha = isHidden ? 0 : 1 } } From f2795c035ad92ebd3a2bc53be7492248c5251c81 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 19:29:48 +0800 Subject: [PATCH 11/12] fix: make blurhash display before video thumbnail ready --- CoreDataStack/Entity/Attachment.swift | 2 +- .../Diffiable/Section/StatusSection.swift | 24 ++++++++++++++- .../View/Container/PlayerContainerView.swift | 30 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CoreDataStack/Entity/Attachment.swift b/CoreDataStack/Entity/Attachment.swift index 16f007bf1..f3f5d262d 100644 --- a/CoreDataStack/Entity/Attachment.swift +++ b/CoreDataStack/Entity/Attachment.swift @@ -27,7 +27,7 @@ public final class Attachment: NSManagedObject { @NSManaged public private(set) var updatedAt: Date @NSManaged public private(set) var index: NSNumber - // many-to-one relastionship + // many-to-one relationship @NSManaged public private(set) var status: Status? } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index 8741ff973..c5d0eb19b 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -375,7 +375,7 @@ extension StatusSection { return containerFrame.width }() let scale: CGFloat = 1.3 - return CGSize(width: maxWidth, height: maxWidth * scale) + return CGSize(width: maxWidth, height: floor(maxWidth * scale)) }() if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, @@ -415,6 +415,28 @@ extension StatusSection { } playerContainerView.isHidden = false + // set blurhash overlay + playerContainerView.isReadyForDisplay + .receive(on: DispatchQueue.main) + .sink { [weak playerContainerView] isReadyForDisplay in + guard let playerContainerView = playerContainerView else { return } + playerContainerView.blurhashOverlayImageView.alpha = isReadyForDisplay ? 0 : 1 + } + .store(in: &cell.disposeBag) + + if let blurhash = videoAttachment.blurhash, + let url = URL(string: videoAttachment.url) { + AppContext.shared.blurhashImageCacheService.image( + blurhash: blurhash, + size: playerContainerView.playerViewController.view.frame.size, + url: url + ) + .sink { image in + playerContainerView.blurhashOverlayImageView.image = image + } + .store(in: &cell.disposeBag) + } + } else { cell.statusView.playerContainerView.playerViewController.player?.pause() cell.statusView.playerContainerView.playerViewController.player = nil diff --git a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift index 1d33572a2..6a3e5d4be 100644 --- a/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/PlayerContainerView.swift @@ -8,6 +8,7 @@ import os.log import AVKit import UIKit +import Combine protocol PlayerContainerViewDelegate: AnyObject { func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) @@ -28,10 +29,14 @@ final class PlayerContainerView: UIView { let playerViewController = AVPlayerViewController() + let blurhashOverlayImageView = UIImageView() let mediaTypeIndicatorView = MediaTypeIndicatorView() weak var delegate: PlayerContainerViewDelegate? + private var isReadyForDisplayObservation: NSKeyValueObservation? + let isReadyForDisplay = CurrentValueSubject(false) + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -64,6 +69,15 @@ extension PlayerContainerView { playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous + blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false + playerViewController.contentOverlayView!.addSubview(blurhashOverlayImageView) + NSLayoutConstraint.activate([ + blurhashOverlayImageView.topAnchor.constraint(equalTo: playerViewController.contentOverlayView!.topAnchor), + blurhashOverlayImageView.leadingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.leadingAnchor), + blurhashOverlayImageView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor), + blurhashOverlayImageView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), + ]) + // mediaType mediaTypeIndicatorView.translatesAutoresizingMaskIntoConstraints = false playerViewController.contentOverlayView!.addSubview(mediaTypeIndicatorView) @@ -74,6 +88,12 @@ extension PlayerContainerView { mediaTypeIndicatorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.width).priority(.required - 1), ]) + isReadyForDisplayObservation = playerViewController.observe(\.isReadyForDisplay, options: [.initial, .new]) { [weak self] playerViewController, _ in + guard let self = self else { return } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isReadyForDisplay: %s", (#file as NSString).lastPathComponent, #line, #function, playerViewController.isReadyForDisplay.description) + self.isReadyForDisplay.value = playerViewController.isReadyForDisplay + } + contentWarningOverlayView.delegate = self } } @@ -94,6 +114,8 @@ extension PlayerContainerView { playerViewController.view.removeFromSuperview() playerViewController.removeFromParent() + blurhashOverlayImageView.image = nil + container.subviews.forEach { subview in subview.removeFromSuperview() } @@ -113,7 +135,7 @@ extension PlayerContainerView { let rect = AVMakeRect( aspectRatio: aspectRatio, insideRect: CGRect(origin: .zero, size: maxSize) - ) + ).integral parent?.addChild(playerViewController) playerViewController.view.translatesAutoresizingMaskIntoConstraints = false @@ -124,11 +146,13 @@ extension PlayerContainerView { playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor), playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor), playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor), - touchBlockingView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1), + touchBlockingView.widthAnchor.constraint(equalToConstant: rect.width).priority(.required - 1), ]) - containerHeightLayoutConstraint.constant = floor(rect.height) + containerHeightLayoutConstraint.constant = rect.height containerHeightLayoutConstraint.isActive = true + playerViewController.view.frame.size = rect.size + contentWarningOverlayView.removeFromSuperview() contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false addSubview(contentWarningOverlayView) From 353e752083599c3de1b84799365c8db7e78c3288 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 17 Jun 2021 19:43:16 +0800 Subject: [PATCH 12/12] feat: trigger timeline fetching after publish post --- .../Scene/HomeTimeline/HomeTimelineViewController.swift | 4 +--- Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 6aed1f574..529f2d81c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -92,15 +92,13 @@ extension HomeTimelineViewController { viewModel.homeTimelineNavigationBarTitleViewModel.state .removeDuplicates() - .debounce(for: 0.3, scheduler: RunLoop.main) - .receive(on: RunLoop.main) + .receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self = self else { return } self.titleView.configure(state: state) } .store(in: &disposeBag) - #if DEBUG // long press to trigger debug menu settingBarButtonItem.menu = debugMenu diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 717519464..fdbbfba9d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -36,6 +36,7 @@ final class HomeTimelineViewModel: NSObject { let timelineIsEmpty = CurrentValueSubject(false) let homeTimelineNeedRefresh = PassthroughSubject() + // output // top loader private(set) lazy var loadLatestStateMachine: GKStateMachine = { @@ -130,6 +131,12 @@ final class HomeTimelineViewModel: NSObject { } .store(in: &disposeBag) + homeTimelineNavigationBarTitleViewModel.isPublished + .sink { [weak self] isPublished in + guard let self = self else { return } + self.homeTimelineNeedRefresh.send() + } + .store(in: &disposeBag) } deinit {