From b41b7403f3edb94962d3d83c2ed608d3e09bad00 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 4 Aug 2021 19:01:02 +0800 Subject: [PATCH 01/13] chore: add stringsdict for i18n --- Localization/Localizable.stringsdict | 138 +++++++++++++++++++++++++++ crowdin.yml | 5 + 2 files changed, 143 insertions(+) create mode 100644 Localization/Localizable.stringsdict diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict new file mode 100644 index 00000000..b002a41c --- /dev/null +++ b/Localization/Localizable.stringsdict @@ -0,0 +1,138 @@ + + + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 reblogs + one + 1 reblog + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + + diff --git a/crowdin.yml b/crowdin.yml index 4b5310c7..3612e371 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -27,6 +27,11 @@ files: [ "translation" : "/Localization/StringsConvertor/input/%locale_with_underscore%/%original_file_name%", "update_option" : "update_as_unapproved", }, + { + "source" : "/Localization/Localizable.stringsdict", + "translation" : "/Localization/StringsConvertor/input/%locale_with_underscore%/%original_file_name%", + "update_option" : "update_as_unapproved", + }, { "source" : "/MastodonIntent/en.lproj/Intents.strings", "translation" : "/Localization/StringsConvertor/Intents/input/%locale_with_underscore%/%original_file_name%", From 09f77c38765c8115ea5384ba3b1ca3ef9acacee3 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 15:24:54 +0800 Subject: [PATCH 02/13] fix: index out bounds issue. resolve #255 --- .../Scene/Share/View/Container/MosaicImageViewContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 8336e852..a6223044 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -207,6 +207,7 @@ extension MosaicImageViewContainer { func setupImageViews(count: Int, maxSize: CGSize) -> [ConfigurableMosaic] { reset() + let count = min(4, max(0, count)) guard count > 1 else { return [] } From 76b033afed8bbe74756ebe9f8a1937b27dba9255 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 16:17:54 +0800 Subject: [PATCH 03/13] feat: make servers meet user preferred language display first --- Mastodon.xcodeproj/project.pbxproj | 17 ++++++++ .../xcschemes/xcschememanagement.plist | 8 ++-- .../xcshareddata/swiftpm/Package.resolved | 9 +++++ ...rverViewModel+LoadIndexedServerState.swift | 8 +--- .../MastodonPickServerViewModel.swift | 40 ++++++++++++++++++- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 025678de..e2fffb8d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -280,6 +280,7 @@ DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; + DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = DB552D4E26BBD10C00E481F6 /* OrderedCollections */; }; DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */; }; DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; }; DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; }; @@ -1311,6 +1312,7 @@ DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, + DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DB01E23326A98F0900C3965B /* MastodonMeta in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, @@ -3081,6 +3083,7 @@ DBC6462A26A1738900B0E31B /* MastodonUI */, DB01E23226A98F0900C3965B /* MastodonMeta */, DB01E23426A98F0900C3965B /* MetaTextKit */, + DB552D4E26BBD10C00E481F6 /* OrderedCollections */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3328,6 +3331,7 @@ DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */, DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */, + DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -5644,6 +5648,14 @@ minimumVersion = 4.1.0; }; }; + DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.5; + }; + }; DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; @@ -5752,6 +5764,11 @@ package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; + DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; DB68050F2637D0F800430867 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 4bf0e25a..09c042d8 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 24 + 26 CoreDataStack.xcscheme_^#shared#^_ orderHint - 25 + 24 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -42,7 +42,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 28 + 27 MastodonIntents.xcscheme_^#shared#^_ @@ -62,7 +62,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 26 + 25 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6a46992..67c613bf 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -136,6 +136,15 @@ "version": "5.11.1" } }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "0959ba76a1d4a98fd11163aa83fd49c25b93bfae", + "version": "0.0.5" + } + }, { "package": "swift-nio", "repositoryURL": "https://github.com/apple/swift-nio.git", diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift index 1cb9b508..0c4910d1 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel+LoadIndexedServerState.swift @@ -55,13 +55,7 @@ extension MastodonPickServerViewModel.LoadIndexedServerState { } receiveValue: { [weak self] response in guard let _ = self else { return } stateMachine.enter(Idle.self) - - // ignore approval required servers - var servers = response.value - if viewModel.mode == .signUp { - servers = servers.filter { !$0.approvalRequired } - } - viewModel.indexedServers.value = servers + viewModel.indexedServers.value = response.value } .store(in: &viewModel.disposeBag) } diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index ef9275b0..7a648011 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -11,6 +11,7 @@ import Combine import GameplayKit import MastodonSDK import CoreDataStack +import OrderedCollections class MastodonPickServerViewModel: NSObject { @@ -167,7 +168,44 @@ extension MastodonPickServerViewModel { searchText.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main).removeDuplicates() ) .map { indexedServers, selectCategoryItem, searchText -> [Mastodon.Entity.Server] in - // Filter the indexed servers from joinmastodon.org + // ignore approval required servers when sign-up + var indexedServers = indexedServers + if self.mode == .signUp { + indexedServers = indexedServers.filter { !$0.approvalRequired } + } + + // group by language user preferred language first. Then sort by `totalUsers` + var languageToServersMapping = OrderedDictionary() + for language in Locale.preferredLanguages { + let local = Locale(identifier: language) + guard let languageCode = local.languageCode else { continue } + // skip if key duplicate + guard !languageToServersMapping.keys.contains(languageCode) else { continue } + // append to dict + languageToServersMapping[languageCode] = indexedServers + .filter { $0.language.lowercased() == languageCode.lowercased() } + .sorted(by: { $0.totalUsers > $1.totalUsers }) + } + // sort remains servers by `totalUsers` + let remainsServers = indexedServers + .filter { server in + return !languageToServersMapping.contains { _, servers in servers.contains(server) } + } + .sorted(by: { $0.totalUsers > $1.totalUsers }) + + var _indexedServers: [Mastodon.Entity.Server] = [] + for key in languageToServersMapping.keys { + _indexedServers.append(contentsOf: languageToServersMapping[key] ?? []) + } + _indexedServers.append(contentsOf: remainsServers) + + if _indexedServers.count == indexedServers.count { + indexedServers = _indexedServers + } else { + assertionFailure("should not change dataset size") + } + + // Filter the indexed servers by category or search text switch selectCategoryItem { case .all: return MastodonPickServerViewModel.filterServers(servers: indexedServers, category: nil, searchText: searchText) From 20c9cd709f1c9ab811e6ad3fd03e41535174f761 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 16:20:35 +0800 Subject: [PATCH 04/13] fix: server cell expand button hit test area too small issue --- .../Onboarding/PickServer/TableViewCell/PickServerCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index 41cf8d38..ee247187 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -87,7 +87,7 @@ class PickServerCell: UITableViewCell { }() let expandButton: UIButton = { - let button = UIButton(type: .custom) + let button = HitTestExpandedButton(type: .custom) button.setImage(UIImage(systemName: "chevron.down", withConfiguration: UIImage.SymbolConfiguration(pointSize: 13)), for: .normal) button.setTitle(L10n.Scene.ServerPicker.Button.seeMore, for: .normal) button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) From a95bd7c7323b841cc246bf27895be7d2dd1465d8 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 16:30:43 +0800 Subject: [PATCH 05/13] fix: menu button too small issue. resolve #235 --- Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift index 794f9c76..f771f8bb 100644 --- a/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift +++ b/Mastodon/Scene/Share/View/ToolBar/ActionToolBarContainer.swift @@ -100,6 +100,8 @@ extension ActionToolbarContainer { button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) button.setInsets(forContentPadding: .zero, imageTitlePadding: style.buttonTitleImagePadding) } + // add more expand for menu button + moreButton.expandEdgeInsets = UIEdgeInsets(top: -10, left: -20, bottom: -10, right: -20) let replyImage = UIImage(systemName: "arrowshape.turn.up.left.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .ultraLight))!.withRenderingMode(.alwaysTemplate) let reblogImage = UIImage(systemName: "arrow.2.squarepath", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .bold))!.withRenderingMode(.alwaysTemplate) From 6ee38d7d11df42312b5f8b2b3d73a7075cb41f00 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 16:34:37 +0800 Subject: [PATCH 06/13] chore: add spacing for title and button. resolve #225 --- .../Search/Search/View/SearchRecommendCollectionHeader.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift index 6c67580f..91ec0128 100644 --- a/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift +++ b/Mastodon/Scene/Search/Search/View/SearchRecommendCollectionHeader.swift @@ -64,18 +64,15 @@ extension SearchRecommendCollectionHeader { ]) let horizontalStackView = UIStackView() + horizontalStackView.spacing = 8 horizontalStackView.axis = .horizontal horizontalStackView.alignment = .center - horizontalStackView.translatesAutoresizingMaskIntoConstraints = false horizontalStackView.distribution = .fill - titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) horizontalStackView.addArrangedSubview(titleLabel) - seeAllButton.translatesAutoresizingMaskIntoConstraints = false horizontalStackView.addArrangedSubview(seeAllButton) containerStackView.addArrangedSubview(horizontalStackView) - descriptionLabel.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(descriptionLabel) } From f7f248b6e98a3dd3625dd01ffbc1857c7ed3170f Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 16:54:00 +0800 Subject: [PATCH 07/13] chore: update haptic feedback for status interaction --- .../StatusProvider/StatusProviderFacade.swift | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index e115151e..4c34c674 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -260,7 +260,7 @@ extension StatusProviderFacade { // haptic feedback generator let generator = UIImpactFeedbackGenerator(style: .light) - let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + let responseFeedbackGenerator = UINotificationFeedbackGenerator() status .compactMap { status -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in @@ -284,13 +284,13 @@ extension StatusProviderFacade { .eraseToAnyPublisher() .switchToLatest() .receive(on: DispatchQueue.main) - .handleEvents { _ in + .handleEvents(receiveSubscription: { _ in generator.prepare() - responseFeedbackGenerator.prepare() - } receiveOutput: { _, favoriteKind in + }, receiveOutput: { _, favoriteKind in generator.impactOccurred() os_log("%{public}s[%{public}ld], %{public}s: [Like] update local status like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike") - } receiveCompletion: { completion in + }, receiveCompletion: { completion in + responseFeedbackGenerator.prepare() switch completion { case .failure: // TODO: handle error @@ -298,7 +298,7 @@ extension StatusProviderFacade { case .finished: break } - } + }) .map { statusID, favoriteKind in return context.apiService.favorite( statusID: statusID, @@ -309,14 +309,13 @@ extension StatusProviderFacade { .switchToLatest() .receive(on: DispatchQueue.main) .sink { [weak provider] completion in - guard let provider = provider else { return } - if provider.view.window != nil { - responseFeedbackGenerator.impactOccurred() - } + guard let _ = provider else { return } switch completion { case .failure(let error): + responseFeedbackGenerator.notificationOccurred(.error) os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: + responseFeedbackGenerator.notificationOccurred(.success) os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request success", ((#file as NSString).lastPathComponent), #line, #function) } } receiveValue: { response in @@ -370,7 +369,7 @@ extension StatusProviderFacade { // haptic feedback generator let generator = UIImpactFeedbackGenerator(style: .light) - let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + let responseFeedbackGenerator = UINotificationFeedbackGenerator() status .compactMap { status -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in @@ -394,10 +393,9 @@ extension StatusProviderFacade { .eraseToAnyPublisher() .switchToLatest() .receive(on: DispatchQueue.main) - .handleEvents { _ in + .handleEvents(receiveSubscription: { _ in generator.prepare() - responseFeedbackGenerator.prepare() - } receiveOutput: { _, reblogKind in + }, receiveOutput: { _, reblogKind in generator.impactOccurred() switch reblogKind { case .reblog: @@ -405,7 +403,8 @@ extension StatusProviderFacade { case .undoReblog: os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog") } - } receiveCompletion: { completion in + }, receiveCompletion: { completion in + responseFeedbackGenerator.prepare() switch completion { case .failure: // TODO: handle error @@ -413,7 +412,7 @@ extension StatusProviderFacade { case .finished: break } - } + }) .map { statusID, reblogKind in return context.apiService.reblog( statusID: statusID, @@ -424,14 +423,13 @@ extension StatusProviderFacade { .switchToLatest() .receive(on: DispatchQueue.main) .sink { [weak provider] completion in - guard let provider = provider else { return } - if provider.view.window != nil { - responseFeedbackGenerator.impactOccurred() - } + guard let _ = provider else { return } switch completion { case .failure(let error): + responseFeedbackGenerator.notificationOccurred(.error) os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) case .finished: + responseFeedbackGenerator.notificationOccurred(.success) os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request success", ((#file as NSString).lastPathComponent), #line, #function) } } receiveValue: { response in From 7f617758c1d69a7580ad041d1572814e88138c05 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 17:20:03 +0800 Subject: [PATCH 08/13] fix: [WIP] bio note edit logic --- .../Header/ProfileHeaderViewController.swift | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 216c8168..8c526392 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -195,32 +195,40 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - Publishers.CombineLatest4( + viewModel.isEditing + .assign(to: \.isEditable, on: profileHeaderView.bioMetaText.textView) + .store(in: &disposeBag) + + viewModel.isEditing + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] isEditing in + guard let self = self else { return } + guard isEditing else { return } + // trigger once when toggle + // and use delegate to update text style + let initialNote = self.viewModel.editProfileInfo.note.value + let metaContent = PlaintextMetaContent(string: initialNote ?? "") + self.profileHeaderView.bioMetaText.configure(content: metaContent) + } + .store(in: &disposeBag) + + Publishers.CombineLatest3( viewModel.isEditing.removeDuplicates(), viewModel.displayProfileInfo.note.removeDuplicates(), - viewModel.editProfileInfo.note.removeDuplicates(), viewModel.emojiMeta.removeDuplicates() ) .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing, note, editingNote, emojiMeta in + .sink { [weak self] isEditing, note, emojiMeta in guard let self = self else { return } - - self.profileHeaderView.bioMetaText.textView.isEditable = isEditing - - if isEditing { - if self.profileHeaderView.bioMetaText.backedString != note { - let metaContent = PlaintextMetaContent(string: editingNote ?? "") - self.profileHeaderView.bioMetaText.configure(content: metaContent) - } - } else { - let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta) - do { - let metaContent = try MastodonMetaContent.convert(document: mastodonContent) - self.profileHeaderView.bioMetaText.configure(content: metaContent) - } catch { - assertionFailure() - self.profileHeaderView.bioMetaText.reset() - } + guard !isEditing else { return } + let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.profileHeaderView.bioMetaText.configure(content: metaContent) + } catch { + assertionFailure() + self.profileHeaderView.bioMetaText.reset() } } .store(in: &disposeBag) @@ -461,9 +469,15 @@ extension ProfileHeaderViewController { extension ProfileHeaderViewController: MetaTextDelegate { func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, metaText.backedString) - assert(metaText.textView === profileHeaderView.bioMetaText.textView) - if metaText.textView === profileHeaderView.bioMetaText.textView { + + switch metaText { + case profileHeaderView.bioMetaText: + guard viewModel.isEditing.value else { break } viewModel.editProfileInfo.note.value = metaText.backedString + let metaContent = PlaintextMetaContent(string: metaText.backedString) + return metaContent + default: + assertionFailure() } return nil From d134178032f141046275cb2058ed5da2abd98748 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 18:11:00 +0800 Subject: [PATCH 09/13] fix: bio editor not stable issue. resolve #265 --- .../Header/ProfileHeaderViewController.swift | 56 ++++++++++--------- .../Header/ProfileHeaderViewModel.swift | 2 + 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 8c526392..716b6230 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -195,43 +195,47 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - viewModel.isEditing - .assign(to: \.isEditable, on: profileHeaderView.bioMetaText.textView) - .store(in: &disposeBag) - - viewModel.isEditing - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing in - guard let self = self else { return } - guard isEditing else { return } - // trigger once when toggle - // and use delegate to update text style - let initialNote = self.viewModel.editProfileInfo.note.value - let metaContent = PlaintextMetaContent(string: initialNote ?? "") - self.profileHeaderView.bioMetaText.configure(content: metaContent) - } - .store(in: &disposeBag) - - Publishers.CombineLatest3( + let profileNote = Publishers.CombineLatest3( viewModel.isEditing.removeDuplicates(), viewModel.displayProfileInfo.note.removeDuplicates(), + viewModel.editProfileInfoDidInitialized + ) + .map { isEditing, displayNote, _ -> String? in + if isEditing { + return self.viewModel.editProfileInfo.note.value + } else { + return displayNote + } + } + .eraseToAnyPublisher() + + Publishers.CombineLatest3( + viewModel.isEditing.removeDuplicates(), + profileNote.removeDuplicates(), viewModel.emojiMeta.removeDuplicates() ) .receive(on: DispatchQueue.main) .sink { [weak self] isEditing, note, emojiMeta in guard let self = self else { return } - guard !isEditing else { return } - let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta) - do { - let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + + self.profileHeaderView.bioMetaText.textView.isEditable = isEditing + + if isEditing { + let metaContent = PlaintextMetaContent(string: note ?? "") self.profileHeaderView.bioMetaText.configure(content: metaContent) - } catch { - assertionFailure() - self.profileHeaderView.bioMetaText.reset() + } else { + let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.profileHeaderView.bioMetaText.configure(content: metaContent) + } catch { + assertionFailure() + self.profileHeaderView.bioMetaText.reset() + } } } .store(in: &disposeBag) + profileHeaderView.bioMetaText.delegate = self NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index c9875858..e8405b6a 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -31,6 +31,7 @@ final class ProfileHeaderViewModel { // output let displayProfileInfo = ProfileInfo() let editProfileInfo = ProfileInfo() + let editProfileInfoDidInitialized = CurrentValueSubject(Void()) // needs trigger initial event let isTitleViewDisplaying = CurrentValueSubject(false) var fieldDiffableDataSource: UICollectionViewDiffableDataSource! @@ -52,6 +53,7 @@ final class ProfileHeaderViewModel { self.editProfileInfo.fields.value = account?.source?.fields?.compactMap { field in ProfileFieldItem.FieldValue(name: field.name, value: field.value) } ?? [] + self.editProfileInfoDidInitialized.send() } .store(in: &disposeBag) From 9242a859dfdb424ff93393db2030c4e19ff63012 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 18:16:50 +0800 Subject: [PATCH 10/13] fix: no audio when phone under Silence Mode issue. resolve #266 --- Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift | 4 ++-- Mastodon/Service/AudioPlaybackService.swift | 4 ++-- Mastodon/Service/VideoPlaybackService.swift | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift index 64bde2e6..af8978c3 100644 --- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift @@ -119,10 +119,10 @@ final class VideoPlayerViewModel { case .unknown, .buffering, .readyToPlay: break case .playing: - try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + try? AVAudioSession.sharedInstance().setCategory(.playback) try? AVAudioSession.sharedInstance().setActive(true) case .paused, .stopped, .failed: - try? AVAudioSession.sharedInstance().setCategory(.ambient) + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) // reset to default try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } } diff --git a/Mastodon/Service/AudioPlaybackService.swift b/Mastodon/Service/AudioPlaybackService.swift index 6fdac4bf..5abb30c6 100644 --- a/Mastodon/Service/AudioPlaybackService.swift +++ b/Mastodon/Service/AudioPlaybackService.swift @@ -39,10 +39,10 @@ final class AudioPlaybackService: NSObject { case .unknown, .buffering, .readyToPlay: break case .playing: - try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) + try? AVAudioSession.sharedInstance().setCategory(.playback) try? AVAudioSession.sharedInstance().setActive(true) case .paused, .stopped, .failed: - try? AVAudioSession.sharedInstance().setCategory(.ambient) + try? AVAudioSession.sharedInstance().setCategory(.soloAmbient) // reset to default try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } } diff --git a/Mastodon/Service/VideoPlaybackService.swift b/Mastodon/Service/VideoPlaybackService.swift index e0ac5e6f..f1e28992 100644 --- a/Mastodon/Service/VideoPlaybackService.swift +++ b/Mastodon/Service/VideoPlaybackService.swift @@ -40,7 +40,6 @@ extension VideoPlaybackService { } else { if latestPlayingVideoPlayerViewModel === playerViewModel { latestPlayingVideoPlayerViewModel = nil -// try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default) } } } From ecb6d2a8094d7f8f9e7a667b3d7e402573aacabd Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 19:28:41 +0800 Subject: [PATCH 11/13] chore: fix follow button can not trigger issue. resolve #263 --- .../Search/RecommendAccountSection.swift | 107 +++++++++++++++++- ...hRecommendAccountsCollectionViewCell.swift | 65 ++++------- .../Search/SearchViewController+Follow.swift | 74 ++++-------- .../Search/Search/SearchViewController.swift | 9 +- .../Scene/Search/Search/SearchViewModel.swift | 13 ++- 5 files changed, 161 insertions(+), 107 deletions(-) diff --git a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift index 64019e58..b894f818 100644 --- a/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift +++ b/Mastodon/Diffiable/Section/Search/RecommendAccountSection.swift @@ -10,6 +10,9 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit +import MetaTextKit +import MastodonMeta +import Combine enum RecommendAccountSection: Equatable, Hashable { case main @@ -18,18 +21,118 @@ enum RecommendAccountSection: Equatable, Hashable { extension RecommendAccountSection { static func collectionViewDiffableDataSource( for collectionView: UICollectionView, + dependency: NeedsDependency, delegate: SearchRecommendAccountsCollectionViewCellDelegate, managedObjectContext: NSManagedObjectContext ) -> UICollectionViewDiffableDataSource { UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak delegate] collectionView, indexPath, objectID -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: SearchRecommendAccountsCollectionViewCell.self), for: indexPath) as! SearchRecommendAccountsCollectionViewCell - let user = managedObjectContext.object(with: objectID) as! MastodonUser + managedObjectContext.performAndWait { + let user = managedObjectContext.object(with: objectID) as! MastodonUser + configure(cell: cell, user: user, dependency: dependency) + } cell.delegate = delegate - cell.config(with: user) return cell } } + static func configure( + cell: SearchRecommendAccountsCollectionViewCell, + user: MastodonUser, + dependency: NeedsDependency + ) { + configureContent(cell: cell, user: user) + + if let currentMastodonUser = dependency.context.authenticationService.activeMastodonAuthentication.value?.user { + configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton) + } + + Publishers.CombineLatest( + ManagedObjectObserver.observe(object: user).eraseToAnyPublisher().mapError { $0 as Error }, + dependency.context.authenticationService.activeMastodonAuthentication.setFailureType(to: Error.self) + ) + .receive(on: DispatchQueue.main) + .sink { _ in + // do nothing + } receiveValue: { [weak cell] change, authentication in + guard let cell = cell else { return } + guard case .update(let object) = change.changeType, + let user = object as? MastodonUser else { return } + guard let currentMastodonUser = authentication?.user else { return } + + configureFollowButton(with: user, currentMastodonUser: currentMastodonUser, followButton: cell.followButton) + } + .store(in: &cell.disposeBag) + + } + + static func configureContent( + cell: SearchRecommendAccountsCollectionViewCell, + user: MastodonUser + ) { + do { + let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + cell.displayNameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback) + cell.displayNameLabel.configure(content: metaContent) + } + cell.acctLabel.text = "@" + user.acct + cell.avatarImageView.af.setImage( + withURL: user.avatarImageURLWithFallback(domain: user.domain), + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) + cell.headerImageView.af.setImage( + withURL: URL(string: user.header)!, + placeholderImage: UIImage.placeholder(color: .systemFill), + imageTransition: .crossDissolve(0.2) + ) { [weak cell] _ in + // guard let cell = cell else { return } + } + } + + static func configureFollowButton( + with mastodonUser: MastodonUser, + currentMastodonUser: MastodonUser, + followButton: HighlightDimmableButton + ) { + let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser) + followButton.setTitle(relationshipActionSet.title, for: .normal) + } + + static func relationShipActionSet( + mastodonUser: MastodonUser, + currentMastodonUser: MastodonUser + ) -> ProfileViewModel.RelationshipActionOptionSet { + var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow]) + let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isFollowing { + relationshipActionSet.insert(.following) + } + + let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isPending { + relationshipActionSet.insert(.pending) + } + + let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false + if isBlocking { + relationshipActionSet.insert(.blocking) + } + + let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false + if isBlockedBy { + relationshipActionSet.insert(.blocked) + } + return relationshipActionSet + } + +} + +extension RecommendAccountSection { + static func tableViewDiffableDataSource( for tableView: UITableView, managedObjectContext: NSManagedObjectContext, diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index cd1d196e..365c1ee7 100644 --- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -5,6 +5,7 @@ // Created by sxiaojian on 2021/4/1. // +import os.log import Combine import CoreDataStack import Foundation @@ -14,12 +15,12 @@ import MetaTextKit import MastodonMeta protocol SearchRecommendAccountsCollectionViewCellDelegate: NSObject { - func followButtonDidPressed(clickedUser: MastodonUser) - - func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) + func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton) } class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { + + let logger = Logger(subsystem: "SearchRecommendAccountsCollectionViewCell", category: "UI") var disposeBag = Set() weak var delegate: SearchRecommendAccountsCollectionViewCellDelegate? @@ -72,7 +73,6 @@ class SearchRecommendAccountsCollectionViewCell: UICollectionViewCell { super.prepareForReuse() headerImageView.af.cancelImageRequest() avatarImageView.af.cancelImageRequest() - visualEffectView.removeFromSuperview() disposeBag.removeAll() } @@ -117,6 +117,15 @@ extension SearchRecommendAccountsCollectionViewCell { headerImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) + headerImageView.addSubview(visualEffectView) + visualEffectView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + visualEffectView.topAnchor.constraint(equalTo: headerImageView.topAnchor), + visualEffectView.leadingAnchor.constraint(equalTo: headerImageView.leadingAnchor), + visualEffectView.trailingAnchor.constraint(equalTo: headerImageView.trailingAnchor), + visualEffectView.bottomAnchor.constraint(equalTo: headerImageView.bottomAnchor) + ]) + let containerStackView = UIStackView() containerStackView.axis = .vertical containerStackView.distribution = .fill @@ -156,48 +165,16 @@ extension SearchRecommendAccountsCollectionViewCell { followButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 24) ]) containerStackView.addArrangedSubview(followButton) + + followButton.addTarget(self, action: #selector(SearchRecommendAccountsCollectionViewCell.followButtonDidPressed(_:)), for: .touchUpInside) } - func config(with mastodonUser: MastodonUser) { - do { - let mastodonContent = MastodonContent(content: mastodonUser.displayNameWithFallback, emojis: mastodonUser.emojiMeta) - let metaContent = try MastodonMetaContent.convert(document: mastodonContent) - displayNameLabel.configure(content: metaContent) - } catch { - let metaContent = PlaintextMetaContent(string: mastodonUser.displayNameWithFallback) - displayNameLabel.configure(content: metaContent) - } - acctLabel.text = "@" + mastodonUser.acct - avatarImageView.af.setImage( - withURL: URL(string: mastodonUser.avatar)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) - headerImageView.af.setImage( - withURL: URL(string: mastodonUser.header)!, - placeholderImage: UIImage.placeholder(color: .systemFill), - imageTransition: .crossDissolve(0.2) - ) { [weak self] _ in - guard let self = self else { return } - self.headerImageView.addSubview(self.visualEffectView) - self.visualEffectView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - self.visualEffectView.topAnchor.constraint(equalTo: self.headerImageView.topAnchor), - self.visualEffectView.leadingAnchor.constraint(equalTo: self.headerImageView.leadingAnchor), - self.visualEffectView.trailingAnchor.constraint(equalTo: self.headerImageView.trailingAnchor), - self.visualEffectView.bottomAnchor.constraint(equalTo: self.headerImageView.bottomAnchor) - ]) - } - delegate?.configFollowButton(with: mastodonUser, followButton: followButton) - followButton.publisher(for: .touchUpInside) - .sink { [weak self] _ in - self?.followButtonDidPressed(mastodonUser: mastodonUser) - } - .store(in: &disposeBag) - } - - func followButtonDidPressed(mastodonUser: MastodonUser) { - delegate?.followButtonDidPressed(clickedUser: mastodonUser) +} + +extension SearchRecommendAccountsCollectionViewCell { + @objc private func followButtonDidPressed(_ sender: UIButton) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + delegate?.searchRecommendAccountsCollectionViewCell(self, followButtonDidPressed: sender) } } diff --git a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift index c345336d..386b0af1 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController+Follow.swift @@ -26,16 +26,30 @@ extension SearchViewController: UserProvider { } extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegate { - func followButtonDidPressed(clickedUser: MastodonUser) { + func searchRecommendAccountsCollectionViewCell(_ cell: SearchRecommendAccountsCollectionViewCell, followButtonDidPressed button: UIButton) { + guard let diffableDataSource = viewModel.accountDiffableDataSource else { return } + guard let indexPath = accountsCollectionView.indexPath(for: cell), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + + context.managedObjectContext.performAndWait { + guard let user = try? context.managedObjectContext.existingObject(with: item) as? MastodonUser else { return } + self.toggleFriendship(for: user) + } + } + + func toggleFriendship(for mastodonUser: MastodonUser) { guard let currentMastodonUser = viewModel.currentMastodonUser.value else { return } - guard let relationshipAction = relationShipActionSet(mastodonUser: clickedUser, currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) else { return } + guard let relationshipAction = RecommendAccountSection.relationShipActionSet( + mastodonUser: mastodonUser, + currentMastodonUser: currentMastodonUser).highPriorityAction(except: .editOptions) + else { return } switch relationshipAction { case .none: break case .follow, .following: - UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserFollowRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // error handling } receiveValue: { _ in @@ -45,7 +59,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat case .pending: break case .muting: - let name = clickedUser.displayNameWithFallback + let name = mastodonUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), @@ -53,7 +67,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserMuteRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // do nothing } receiveValue: { _ in @@ -66,7 +80,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat alertController.addAction(cancelAction) present(alertController, animated: true, completion: nil) case .blocking: - let name = clickedUser.displayNameWithFallback + let name = mastodonUser.displayNameWithFallback let alertController = UIAlertController( title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title, message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name), @@ -74,7 +88,7 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat ) let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in guard let self = self else { return } - UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: clickedUser) + UserProviderFacade.toggleUserBlockRelationship(provider: self, mastodonUser: mastodonUser) .sink { _ in // do nothing } receiveValue: { _ in @@ -93,50 +107,4 @@ extension SearchViewController: SearchRecommendAccountsCollectionViewCellDelegat } } - func configFollowButton(with mastodonUser: MastodonUser, followButton: HighlightDimmableButton) { - guard let currentMastodonUser = viewModel.currentMastodonUser.value else { - return - } - _configFollowButton(with: mastodonUser, currentMastodonUser: currentMastodonUser, followButton: followButton) - ManagedObjectObserver.observe(object: currentMastodonUser) - .sink { _ in - - } receiveValue: { change in - guard case .update(let object) = change.changeType, - let newUser = object as? MastodonUser else { return } - self._configFollowButton(with: mastodonUser, currentMastodonUser: newUser, followButton: followButton) - } - .store(in: &disposeBag) - } -} - -extension SearchViewController { - func _configFollowButton(with mastodonUser: MastodonUser, currentMastodonUser: MastodonUser, followButton: HighlightDimmableButton) { - let relationshipActionSet = relationShipActionSet(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser) - followButton.setTitle(relationshipActionSet.title, for: .normal) - } - - func relationShipActionSet(mastodonUser: MastodonUser, currentMastodonUser: MastodonUser) -> ProfileViewModel.RelationshipActionOptionSet { - var relationshipActionSet = ProfileViewModel.RelationshipActionOptionSet([.follow]) - let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isFollowing { - relationshipActionSet.insert(.following) - } - - let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isPending { - relationshipActionSet.insert(.pending) - } - - let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false - if isBlocking { - relationshipActionSet.insert(.blocking) - } - - let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false - if isBlockedBy { - relationshipActionSet.insert(.blocked) - } - return relationshipActionSet - } } diff --git a/Mastodon/Scene/Search/Search/SearchViewController.swift b/Mastodon/Scene/Search/Search/SearchViewController.swift index 7c500370..a3d84cd6 100644 --- a/Mastodon/Scene/Search/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/Search/SearchViewController.swift @@ -40,7 +40,7 @@ final class SearchViewController: UIViewController, NeedsDependency { var searchTransitionController = SearchTransitionController() var disposeBag = Set() - private(set) lazy var viewModel = SearchViewModel(context: context, coordinator: coordinator) + private(set) lazy var viewModel = SearchViewModel(context: context) // recommend let scrollView: UIScrollView = { @@ -167,7 +167,12 @@ extension SearchViewController { private func setupDataSource() { viewModel.hashtagDiffableDataSource = RecommendHashTagSection.collectionViewDiffableDataSource(for: hashtagCollectionView) - viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource(for: accountsCollectionView, delegate: self, managedObjectContext: context.managedObjectContext) + viewModel.accountDiffableDataSource = RecommendAccountSection.collectionViewDiffableDataSource( + for: accountsCollectionView, + dependency: self, + delegate: self, + managedObjectContext: context.managedObjectContext + ) } } diff --git a/Mastodon/Scene/Search/Search/SearchViewModel.swift b/Mastodon/Scene/Search/Search/SearchViewModel.swift index 4929ccca..1c245609 100644 --- a/Mastodon/Scene/Search/Search/SearchViewModel.swift +++ b/Mastodon/Scene/Search/Search/SearchViewModel.swift @@ -19,24 +19,25 @@ final class SearchViewModel: NSObject { // input let context: AppContext - weak var coordinator: SceneCoordinator! - - let currentMastodonUser = CurrentValueSubject(nil) let viewDidAppeared = PassthroughSubject() // output + let currentMastodonUser = CurrentValueSubject(nil) - // var recommendHashTags = [Mastodon.Entity.Tag]() var recommendAccounts = [NSManagedObjectID]() var recommendAccountsFallback = PassthroughSubject() var hashtagDiffableDataSource: UICollectionViewDiffableDataSource? var accountDiffableDataSource: UICollectionViewDiffableDataSource? - init(context: AppContext, coordinator: SceneCoordinator) { - self.coordinator = coordinator + init(context: AppContext) { self.context = context super.init() + + context.authenticationService.activeMastodonAuthentication + .map { $0?.user } + .assign(to: \.value, on: currentMastodonUser) + .store(in: &disposeBag) Publishers.CombineLatest( context.authenticationService.activeMastodonAuthenticationBox, From 670bc295e19da1781ee75ad352eb4d2c33aa0c42 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 19:43:08 +0800 Subject: [PATCH 12/13] chore: update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d8f7a9c6..e31c4879 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay) - [Nuke](https://github.com/kean/Nuke) - [Pageboy](https://github.com/uias/Pageboy#the-basics) - [SDWebImage](https://github.com/SDWebImage/SDWebImage) +- [swift-collections](https://github.com/apple/swift-collections) - [swift-nio](https://github.com/apple/swift-nio) - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) From a52a09a71bdcf136236709a007315031aedef08b Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 5 Aug 2021 19:58:56 +0800 Subject: [PATCH 13/13] chore: update version to 1.0.3 (53) --- Mastodon.xcodeproj/project.pbxproj | 64 +++++++++---------- .../xcschemes/xcschememanagement.plist | 8 +-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e2fffb8d..271016b9 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4486,7 +4486,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4494,7 +4494,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4513,7 +4513,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4521,7 +4521,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4776,7 +4776,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4784,7 +4784,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4800,7 +4800,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4808,7 +4808,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4824,7 +4824,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4832,7 +4832,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4848,7 +4848,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4856,7 +4856,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.MastodonIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4872,7 +4872,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4880,7 +4880,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4896,7 +4896,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4904,7 +4904,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4920,7 +4920,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4928,7 +4928,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -4944,7 +4944,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4952,7 +4952,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -5034,7 +5034,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5042,7 +5042,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5148,7 +5148,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5156,7 +5156,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -5268,7 +5268,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5276,7 +5276,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5382,7 +5382,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5390,7 +5390,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -5436,7 +5436,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5444,7 +5444,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -5459,7 +5459,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5467,7 +5467,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 09c042d8..10ea3fcf 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 26 + 27 CoreDataStack.xcscheme_^#shared#^_ orderHint - 24 + 25 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -42,7 +42,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 27 + 24 MastodonIntents.xcscheme_^#shared#^_ @@ -62,7 +62,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 25 + 26 SuppressBuildableAutocreation