From fa88cbcc96688a6fc37bc44f9c44511940a6fd55 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 15 May 2023 10:44:00 +0800 Subject: [PATCH 01/17] fix: duplicated trend items may raise crash issue --- TwidereSDK/Sources/TwidereCore/Service/TrendService.swift | 4 ++-- TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift b/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift index 7841b62e..3bf926bf 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/TrendService.swift @@ -87,7 +87,7 @@ extension TrendService { for data in response.value { guard let location = data.locations.first else { continue } trendGroupRecords[.twitter(placeID: location.woeid)] = TrendGroup( - trends: data.trends.map { TrendObject.twitter(trend: $0) }, + trends: data.trends.map { TrendObject.twitter(trend: $0) }.removingDuplicates(), timestamp: data.asOf ) } @@ -97,7 +97,7 @@ extension TrendService { authenticationContext: authenticationContext ) trendGroupRecords[.mastodon(domain: domain)] = TrendGroup( - trends: response.value.map { TrendObject.mastodon(tag: $0) }, + trends: response.value.map { TrendObject.mastodon(tag: $0) }.removingDuplicates(), timestamp: response.networkDate ) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch trends \(response.value.count) ") diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift index 1cb529d7..1897af57 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift @@ -31,7 +31,9 @@ extension TrendViewModel { .map { trendGroupRecords, trendGroupIndex in let trendItems: [SearchItem] = trendGroupRecords[trendGroupIndex] .flatMap { group in - return group.trends.map { .trend(trend: $0) } + return group.trends + .removingDuplicates() + .map { .trend(trend: $0) } } ?? [] return trendItems } From fac7dbc46ede3617775a7c0dd982a2dcaa2eab45 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:10:58 +0800 Subject: [PATCH 02/17] chore: update version to 2.0.0 (130) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 4f6e87f4..685ee726 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 129 + 130 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 8810f44f..1d95e087 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 9840a86c..106f8f28 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 129; + CURRENT_PROJECT_VERSION = 130; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 5f7a8ad6..ca828d48 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 14cabbb5..09edc087 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index f33f97dc..45d40d23 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index f33f97dc..45d40d23 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 129 + 130 From b4ecefadc0ce4149aa259e628f2f449838a2cc88 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:11:43 +0800 Subject: [PATCH 03/17] fix: restore v1 lookup for Home timeline --- .../Timeline/APIService+Timeline+Home.swift | 23 +++ .../Timeline/APIService+Timeline.swift | 174 +++++++++--------- 2 files changed, 110 insertions(+), 87 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift index e88f1bcb..d0a93ac7 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Home.swift @@ -129,6 +129,18 @@ extension APIService { return TwitterHomeTimelineTaskResult.content(response) } } + // fetch lookup + group.addTask { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch lookup") + let responses = await self.twitterBatchLookupResponses( + content: response.value, + authenticationContext: authenticationContext + ) + #if DEBUG + response.logRateLimit(category: "HomeLookup") + #endif + return TwitterHomeTimelineTaskResult.lookup(responses) + } case .lookup: break case .persist: @@ -159,6 +171,17 @@ extension APIService { let statusArray = statusRecords.compactMap { $0.object(in: managedObjectContext) } assert(statusArray.count == statusRecords.count) + // amend the v2 missing properties + if let me = me { + var batchLookupResponse = TwitterBatchLookupResponse() + for lookupResult in lookupResults { + for status in lookupResult.value { + batchLookupResponse.lookupDict[status.idStr] = status + } + } + batchLookupResponse.update(statuses: statusArray, me: me) + } + // locate anchor status let anchorStatus: TwitterStatus? = { guard let untilID = query.untilID else { return nil } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift index 127bf5ac..736a3909 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift @@ -127,90 +127,90 @@ extension APIService { } // end func } -//// Fetch v1 API again to update v2 missing properies -//extension APIService { -// -// public struct TwitterBatchLookupResponse { -// let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") -// -// public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] -// -// public func update(status: TwitterStatus, me: TwitterUser) { -// guard let lookupStatus = lookupDict[status.id] else { return } -// -// // like state -// lookupStatus.favorited.flatMap { -// status.update(isLike: $0, by: me) -// } -// // repost state -// lookupStatus.retweeted.flatMap { -// status.update(isRepost: $0, by: me) -// } -// // media -// if let twitterAttachments = lookupStatus.twitterAttachments { -// // gif -// let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) -// if isGIF { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") -// status.update(attachments: twitterAttachments) -// return -// } -// // media missing bug -// if status.attachments.isEmpty { -// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") -// status.update(attachments: twitterAttachments) -// return -// } -// } -// } -// -// public func update(statuses: [TwitterStatus], me: TwitterUser) { -// for status in statuses { -// update(status: status, me: me) -// } -// } -// } -// -// public func twitterBatchLookupResponses( -// statusIDs: [Twitter.Entity.Tweet.ID], -// authenticationContext: TwitterAuthenticationContext -// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { -// let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { -// statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in -// let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) -// let response = try? await Twitter.API.Lookup.tweets( -// session: self.session, -// query: query, -// authorization: authenticationContext.authorization -// ) -// return response -// } -// -// return _responses.compactMap { $0 } -// } -// -// public func twitterBatchLookupResponses( -// content: Twitter.API.V2.User.Timeline.HomeContent, -// authenticationContext: TwitterAuthenticationContext -// ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { -// let statusIDs: [Twitter.Entity.Tweet.ID] = { -// var ids: [Twitter.Entity.Tweet.ID] = [] -// ids.append(contentsOf: content.data?.map { $0.id } ?? []) -// ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) -// return ids -// }() -// -// let responses = await twitterBatchLookupResponses( -// statusIDs: statusIDs, -// authenticationContext: authenticationContext -// ) -// -// return responses -// } -// +// Fetch v1 API again to update v2 missing properies +extension APIService { + + public struct TwitterBatchLookupResponse { + let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponse") + + public var lookupDict: [Twitter.Entity.Tweet.ID: Twitter.Entity.Tweet] = [:] + + public func update(status: TwitterStatus, me: TwitterUser) { + guard let lookupStatus = lookupDict[status.id] else { return } + + // like state + lookupStatus.favorited.flatMap { + status.update(isLike: $0, by: me) + } + // repost state + lookupStatus.retweeted.flatMap { + status.update(isRepost: $0, by: me) + } + // media + if let twitterAttachments = lookupStatus.twitterAttachments { + // gif + let isGIF = twitterAttachments.contains(where: { $0.kind == .animatedGIF }) + if isGIF { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix GIF missing") + status.update(attachmentsTransient: twitterAttachments) + return + } + // media missing bug + if status.attachmentsTransient.isEmpty { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fix media missing") + status.update(attachmentsTransient: twitterAttachments) + return + } + } + } + + public func update(statuses: [TwitterStatus], me: TwitterUser) { + for status in statuses { + update(status: status, me: me) + } + } + } + + public func twitterBatchLookupResponses( + statusIDs: [Twitter.Entity.Tweet.ID], + authenticationContext: TwitterAuthenticationContext + ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { + let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { + statusIDs[$0.. Twitter.Response.Content<[Twitter.Entity.Tweet]>? in + let query = Twitter.API.Lookup.LookupQuery(ids: Array(chunk)) + let response = try? await Twitter.API.Lookup.tweets( + session: self.session, + query: query, + authorization: authenticationContext.authorization + ) + return response + } + + return _responses.compactMap { $0 } + } + + public func twitterBatchLookupResponses( + content: Twitter.API.V2.User.Timeline.HomeContent, + authenticationContext: TwitterAuthenticationContext + ) async -> [Twitter.Response.Content<[Twitter.Entity.Tweet]>] { + let statusIDs: [Twitter.Entity.Tweet.ID] = { + var ids: [Twitter.Entity.Tweet.ID] = [] + ids.append(contentsOf: content.data?.map { $0.id } ?? []) + ids.append(contentsOf: content.includes?.tweets?.map { $0.id } ?? []) + return ids + }() + + let responses = await twitterBatchLookupResponses( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + return responses + } + // public func twitterBatchLookup( // statusIDs: [Twitter.Entity.Tweet.ID], // authenticationContext: TwitterAuthenticationContext @@ -229,9 +229,9 @@ extension APIService { // // return .init(lookupDict: lookupDict) // } -// -//} -// + +} + //// Fetch v2 API again to update v2 only properies //extension APIService { // From 1c8ee710273a19ee632bf927a1f2043e91ce7ea5 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:12:44 +0800 Subject: [PATCH 04/17] fix: add missing like relationship when fetch like timeline --- .../APIService/Timeline/APIService+Timeline+Like.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift index a028b6d8..0119dfd0 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift @@ -55,7 +55,7 @@ extension APIService { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user // persist [TwitterStatus] - _ = Persistence.Twitter.persist( + let results = Persistence.Twitter.persist( in: managedObjectContext, context: Persistence.Twitter.PersistContextV2( dictionary: dictionary, @@ -63,6 +63,11 @@ extension APIService { networkDate: response.networkDate ) ) + if let me = me, userID == me.id { + for result in results { + result.update(isLike: true, by: me) + } + } } // end try await managedObjectContext.performChanges return response From d5d0a3e64efb14f79b1ecd26046cf492a9cd68e0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:14:04 +0800 Subject: [PATCH 05/17] feat: update the TrendView --- .../Entity/Mastodon+Entity+History.swift | 6 + .../Entity/Mastodon+Entity+Tag.swift | 1 + .../Sources/TwidereUI/Content/TrendView.swift | 192 ++++++++++++++++++ .../Diffable/Misc/Search/SearchSection.swift | 74 ++++--- .../SavedSearchViewController.swift | 23 +++ .../SavedSearchViewModel+Diffable.swift | 4 +- .../SavedSearch/SavedSearchViewModel.swift | 2 + .../Search/Cell/TrendTableViewCell.swift | 181 +++++++++-------- .../Search/Search/SearchViewController.swift | 23 +++ .../Search/SearchViewModel+Diffable.swift | 4 +- .../Scene/Search/Search/SearchViewModel.swift | 2 + .../Search/Trend/TrendViewController.swift | 23 +++ .../Trend/TrendViewModel+Diffable.swift | 4 +- .../Scene/Search/Trend/TrendViewModel.swift | 2 + 14 files changed, 416 insertions(+), 125 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereUI/Content/TrendView.swift diff --git a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 4e3a6640..5374bd08 100644 --- a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -21,5 +21,11 @@ extension Mastodon.Entity { public let day: Date public let uses: String public let accounts: String + + public init(day: Date, uses: String, accounts: String) { + self.day = day + self.uses = uses + self.accounts = accounts + } } } diff --git a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index 8d8e0d81..5518b6e6 100644 --- a/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/TwidereSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -22,6 +22,7 @@ extension Mastodon.Entity { public let url: String public let history: [History]? + enum CodingKeys: String, CodingKey { case name case url diff --git a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift new file mode 100644 index 00000000..e6cad025 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift @@ -0,0 +1,192 @@ +// +// SwiftUIView.swift +// +// +// Created by MainasuK on 2023/5/15. +// + +import SwiftUI +import Combine +import Meta +import MetaTextKit +import TwitterSDK +import MastodonSDK + +public struct TrendView: View { + + @ObservedObject public var viewModel: ViewModel + + public init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: .zero) { + titleLabel + descriptionLabel + } + Spacer() + HStack { + chartDescriptionLabel + chartView + } + } + } +} + +extension TrendView { + var titleLabel: some View { + LabelRepresentable( + metaContent: viewModel.title, + textStyle: .searchTrendTitle, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + } + + var descriptionLabel: some View { + LabelRepresentable( + metaContent: viewModel.description, + textStyle: .searchTrendSubtitle, + setupLabel: { label in + label.setContentHuggingPriority(.defaultHigh, for: .horizontal) + label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + ) + .fixedSize(horizontal: false, vertical: true) + } + + var chartDescriptionLabel: some View { + Text(viewModel.chartDescription) + .font(.footnote) + .foregroundColor(.secondary) + } + + var chartView: some View { + EmptyView() +// Chart { +// +// } + } +} + +extension TrendView { + public class ViewModel: ObservableObject { + + @Published public var viewLayoutFrame = ViewLayoutFrame() + + // input + public let kind: Kind + public let title: MetaContent + public let description: MetaContent + public let chartDescription: String + + // output + @Published var historyData: [Mastodon.Entity.History]? + + public init( + kind: Kind, + title: MetaContent, + description: MetaContent, + chartDescription: String, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.kind = kind + self.title = title + self.description = description + self.chartDescription = chartDescription + // end init + + viewLayoutFramePublisher?.assign(to: &$viewLayoutFrame) + } + + } +} + +extension TrendView.ViewModel { + public enum Kind: Hashable { + case twitter + case mastodon + } +} + +extension TrendView.ViewModel { + public convenience init( + object: TrendObject, + viewLayoutFramePublisher: Published.Publisher? + ) { + switch object { + case .twitter(let trend): + self.init(trend: trend, viewLayoutFramePublisher: viewLayoutFramePublisher) + case .mastodon(let tag): + self.init(tag: tag, viewLayoutFramePublisher: viewLayoutFramePublisher) + } + } + + public convenience init( + trend: Twitter.Entity.Trend, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: .twitter, + title: PlaintextMetaContent(string: "\(trend.name)"), + description: PlaintextMetaContent(string: ""), + chartDescription: "", + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + } + + public convenience init( + tag: Mastodon.Entity.Tag, + viewLayoutFramePublisher: Published.Publisher? + ) { + self.init( + kind: .mastodon, + title: Meta.convert(document: .plaintext(string: "#" + tag.name)), + description: PlaintextMetaContent(string: L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0)), + chartDescription: tag.history?.first?.uses ?? " ", + viewLayoutFramePublisher: viewLayoutFramePublisher + ) + + self.historyData = tag.history + } +} + + +#if DEBUG +extension TrendView.ViewModel { + convenience init(kind: Kind) { + self.init( + kind: kind, + title: PlaintextMetaContent(string: "#Name"), + description: PlaintextMetaContent(string: "500 people talking"), + chartDescription: "123", + viewLayoutFramePublisher: nil + ) + + historyData = [ + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "123"), + ] + } +} +#endif + +#if canImport(SwiftUI) && DEBUG +struct TrendView_Previews: PreviewProvider { + static var previews: some View { + TrendView(viewModel: .init(kind: .twitter)) + TrendView(viewModel: .init(kind: .mastodon)) + } +} +#endif + diff --git a/TwidereX/Diffable/Misc/Search/SearchSection.swift b/TwidereX/Diffable/Misc/Search/SearchSection.swift index 34b30ca6..315934d9 100644 --- a/TwidereX/Diffable/Misc/Search/SearchSection.swift +++ b/TwidereX/Diffable/Misc/Search/SearchSection.swift @@ -7,8 +7,10 @@ // import UIKit +import SwiftUI import Meta import TwidereCore +import TwidereUI enum SearchSection: Hashable, CaseIterable { case history @@ -17,7 +19,9 @@ enum SearchSection: Hashable, CaseIterable { extension SearchSection { - struct Configuration { } + struct Configuration { + let viewLayoutFramePublisher: Published.Publisher? + } static func diffableDataSource( tableView: UITableView, @@ -41,7 +45,13 @@ extension SearchSection { return cell case .trend(let object): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TrendTableViewCell.self), for: indexPath) as! TrendTableViewCell - configure(cell: cell, object: object) + let trendViewModel = TrendView.ViewModel( + object: object, + viewLayoutFramePublisher: configuration.viewLayoutFramePublisher + ) + cell.contentConfiguration = UIHostingConfiguration { + TrendView(viewModel: trendViewModel) + } return cell case .loader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell @@ -84,34 +94,34 @@ extension SearchSection { } } - private static func configure( - cell: TrendTableViewCell, - object: TrendObject - ) { - switch object { - case .twitter(let trend): - let metaContent = Meta.convert(document: .plaintext(string: trend.name)) - cell.primaryLabel.configure(content: metaContent) - cell.accessoryType = .disclosureIndicator - case .mastodon(let tag): - let metaContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) - - cell.primaryLabel.configure(content: metaContent) - cell.secondaryLabel.text = L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0) - cell.setSecondaryLabelDisplay() - - cell.supplementaryLabel.text = tag.history?.first?.uses ?? " " - cell.setSupplementaryLabelDisplay() - - cell.lineChartView.data = (tag.history ?? []) - .sorted(by: { $0.day < $1.day }) // latest last - .map { entry in - guard let point = Int(entry.accounts) else { - return .zero - } - return CGFloat(point) - } - cell.setLineChartViewDisplay() - } - } +// private static func configure( +// cell: TrendTableViewCell, +// object: TrendObject +// ) { +// switch object { +// case .twitter(let trend): +// let metaContent = Meta.convert(document: .plaintext(string: trend.name)) +// cell.primaryLabel.configure(content: metaContent) +// cell.accessoryType = .disclosureIndicator +// case .mastodon(let tag): +// let metaContent = Meta.convert(document: .plaintext(string: "#" + tag.name)) +// +// cell.primaryLabel.configure(content: metaContent) +// cell.secondaryLabel.text = L10n.Scene.Trends.accounts(tag.talkingPeopleCount ?? 0) +// cell.setSecondaryLabelDisplay() +// +// cell.supplementaryLabel.text = tag.history?.first?.uses ?? " " +// cell.setSupplementaryLabelDisplay() +// +// cell.lineChartView.data = (tag.history ?? []) +// .sorted(by: { $0.day < $1.day }) // latest last +// .map { entry in +// guard let point = Int(entry.accounts) else { +// return .zero +// } +// return CGFloat(point) +// } +// cell.setLineChartViewDisplay() +// } +// } } diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift index 92af6493..f0a4b06a 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift @@ -62,6 +62,29 @@ extension SavedSearchViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - AuthContextProvider diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift index 98b252c0..dfcdc305 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel+Diffable.swift @@ -16,7 +16,9 @@ extension SavedSearchViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift index afa96c39..2252bc6e 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift @@ -22,6 +22,8 @@ final class SavedSearchViewModel { let authContext: AuthContext let savedSearchService: SavedSearchService let savedSearchFetchedResultController: SavedSearchFetchedResultController + + @Published public var viewLayoutFrame = ViewLayoutFrame() // output var diffableDataSource: UITableViewDiffableDataSource? diff --git a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift index 7aca31b3..988919cb 100644 --- a/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift +++ b/TwidereX/Scene/Search/Search/Cell/TrendTableViewCell.swift @@ -13,35 +13,36 @@ import TwidereCore final class TrendTableViewCell: UITableViewCell { - let container: UIStackView = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 16 - return stackView - }() - - let infoContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView - }() - - let lineChartContainer: UIStackView = { - let stackView = UIStackView() - stackView.axis = .vertical - return stackView - }() - - let primaryLabel = MetaLabel(style: .searchTrendTitle) - let secondaryLabel = PlainLabel(style: .searchTrendSubtitle) - let supplementaryLabel = PlainLabel(style: .searchTrendCount) - let lineChartView = LineChartView() +// let container: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .horizontal +// stackView.spacing = 16 +// return stackView +// }() +// +// let infoContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// return stackView +// }() +// +// let lineChartContainer: UIStackView = { +// let stackView = UIStackView() +// stackView.axis = .vertical +// return stackView +// }() +// +// let primaryLabel = MetaLabel(style: .searchTrendTitle) +// let secondaryLabel = PlainLabel(style: .searchTrendSubtitle) +// let supplementaryLabel = PlainLabel(style: .searchTrendCount) +// let lineChartView = LineChartView() override func prepareForReuse() { super.prepareForReuse() - accessoryType = .none - resetDisplay() + contentConfiguration = nil +// accessoryType = .none +// resetDisplay() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -59,72 +60,72 @@ final class TrendTableViewCell: UITableViewCell { extension TrendTableViewCell { private func _init() { - container.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(container) - NSLayoutConstraint.activate([ - container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), - container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11), - ]) - - // container: H - [ info container | padding | supplementary | line chart container ] - container.addArrangedSubview(infoContainer) - - // info container: V - [ primary | secondary ] - infoContainer.addArrangedSubview(primaryLabel) - infoContainer.addArrangedSubview(secondaryLabel) - - // padding - let padding = UIView() - container.addArrangedSubview(padding) - - // supplementary - container.addArrangedSubview(supplementaryLabel) - supplementaryLabel.setContentHuggingPriority(.required - 1, for: .horizontal) - - // line chart - container.addArrangedSubview(lineChartContainer) - - let lineChartViewTopPadding = UIView() - let lineChartViewBottomPadding = UIView() - lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false - lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false - lineChartView.translatesAutoresizingMaskIntoConstraints = false - lineChartContainer.addArrangedSubview(lineChartViewTopPadding) - lineChartContainer.addArrangedSubview(lineChartView) - lineChartContainer.addArrangedSubview(lineChartViewBottomPadding) - NSLayoutConstraint.activate([ - lineChartView.widthAnchor.constraint(equalToConstant: 66), - lineChartView.heightAnchor.constraint(equalToConstant: 27), - lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor), - ]) - - primaryLabel.isUserInteractionEnabled = false - - resetDisplay() +// container.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(container) +// NSLayoutConstraint.activate([ +// container.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), +// container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), +// container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), +// contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11), +// ]) +// +// // container: H - [ info container | padding | supplementary | line chart container ] +// container.addArrangedSubview(infoContainer) +// +// // info container: V - [ primary | secondary ] +// infoContainer.addArrangedSubview(primaryLabel) +// infoContainer.addArrangedSubview(secondaryLabel) +// +// // padding +// let padding = UIView() +// container.addArrangedSubview(padding) +// +// // supplementary +// container.addArrangedSubview(supplementaryLabel) +// supplementaryLabel.setContentHuggingPriority(.required - 1, for: .horizontal) +// +// // line chart +// container.addArrangedSubview(lineChartContainer) +// +// let lineChartViewTopPadding = UIView() +// let lineChartViewBottomPadding = UIView() +// lineChartViewTopPadding.translatesAutoresizingMaskIntoConstraints = false +// lineChartViewBottomPadding.translatesAutoresizingMaskIntoConstraints = false +// lineChartView.translatesAutoresizingMaskIntoConstraints = false +// lineChartContainer.addArrangedSubview(lineChartViewTopPadding) +// lineChartContainer.addArrangedSubview(lineChartView) +// lineChartContainer.addArrangedSubview(lineChartViewBottomPadding) +// NSLayoutConstraint.activate([ +// lineChartView.widthAnchor.constraint(equalToConstant: 66), +// lineChartView.heightAnchor.constraint(equalToConstant: 27), +// lineChartViewTopPadding.heightAnchor.constraint(equalTo: lineChartViewBottomPadding.heightAnchor), +// ]) +// +// primaryLabel.isUserInteractionEnabled = false +// +// resetDisplay() } } -extension TrendTableViewCell { - - func resetDisplay() { - secondaryLabel.isHidden = true - supplementaryLabel.isHidden = true - lineChartContainer.isHidden = true - } - - func setSecondaryLabelDisplay() { - secondaryLabel.isHidden = false - } - - func setSupplementaryLabelDisplay() { - supplementaryLabel.isHidden = false - } - - func setLineChartViewDisplay() { - lineChartContainer.isHidden = false - } - -} +//extension TrendTableViewCell { +// +// func resetDisplay() { +// secondaryLabel.isHidden = true +// supplementaryLabel.isHidden = true +// lineChartContainer.isHidden = true +// } +// +// func setSecondaryLabelDisplay() { +// secondaryLabel.isHidden = false +// } +// +// func setSupplementaryLabelDisplay() { +// supplementaryLabel.isHidden = false +// } +// +// func setLineChartViewDisplay() { +// lineChartContainer.isHidden = false +// } +// +//} diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 3d9853bf..30c52b4a 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -167,6 +167,29 @@ extension SearchViewController { viewModel.viewDidAppear.send() } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } extension SearchViewController { diff --git a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift index 55602440..1774973f 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift @@ -17,7 +17,9 @@ extension SearchViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/Search/SearchViewModel.swift b/TwidereX/Scene/Search/Search/SearchViewModel.swift index 9a89fcd0..2233dbcf 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel.swift @@ -26,6 +26,8 @@ final class SearchViewModel { let trendViewModel: TrendViewModel let viewDidAppear = PassthroughSubject() + @Published public var viewLayoutFrame = ViewLayoutFrame() + // output var diffableDataSource: UITableViewDiffableDataSource? @Published var savedSearchTexts = Set() diff --git a/TwidereX/Scene/Search/Trend/TrendViewController.swift b/TwidereX/Scene/Search/Trend/TrendViewController.swift index cc3b33a0..605dd07d 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewController.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewController.swift @@ -68,6 +68,29 @@ extension TrendViewController { tableView.deselectRow(with: transitionCoordinator, animated: animated) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + viewModel.viewLayoutFrame.update(view: view) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate {[weak self] _ in + guard let self = self else { return } + self.viewModel.viewLayoutFrame.update(view: self.view) + } completion: { _ in + // do nothing + } + } + } // MARK: - AuthContextProvider diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift index 1897af57..675b69ea 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel+Diffable.swift @@ -16,7 +16,9 @@ extension TrendViewModel { diffableDataSource = SearchSection.diffableDataSource( tableView: tableView, context: context, - configuration: SearchSection.Configuration() + configuration: SearchSection.Configuration( + viewLayoutFramePublisher: $viewLayoutFrame + ) ) var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel.swift b/TwidereX/Scene/Search/Trend/TrendViewModel.swift index ad08cfb1..4e5d6c0f 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel.swift @@ -24,6 +24,8 @@ final class TrendViewModel: ObservableObject { let trendService: TrendService @Published var trendGroupIndex: TrendService.TrendGroupIndex = .none @Published var searchText = "" + + @Published public var viewLayoutFrame = ViewLayoutFrame() // output var diffableDataSource: UITableViewDiffableDataSource? From 69762a0d2fc2dbb3740198e3d9506c8847995f18 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 17:14:57 +0800 Subject: [PATCH 06/17] feat: use transient property to lazy load the JSON data from objects --- .../.xccurrentversion | 2 +- .../CoreDataStack 8.xcdatamodel/contents | 322 ++++++++++++++++++ .../Entity/Twitter/TwitterStatus.swift | 202 +++++++---- .../CoreDataStack/TwitterStatus.swift | 6 +- .../Model/Status/StatusObject.swift | 2 +- .../Persistence+TwitterStatus+V2.swift | 10 +- .../Twitter/Persistence+TwitterStatus.swift | 8 +- .../Content/MediaView+ViewModel.swift | 2 +- .../Content/StatusView+ViewModel.swift | 4 +- .../ComposeContentViewModel.swift | 4 +- .../Facade/DataSourceFacade+Profile.swift | 2 +- ...meTimelineViewController+DebugAction.swift | 6 +- 12 files changed, 476 insertions(+), 94 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion index eaddb16f..c7412cd8 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreDataStack 7.xcdatamodel + CoreDataStack 8.xcdatamodel diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents new file mode 100644 index 00000000..1459ebff --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index 213195a5..dbc50a0f 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -83,107 +83,167 @@ final public class TwitterStatus: NSManagedObject { } extension TwitterStatus { + @NSManaged private var attachments: Data? + @NSManaged private var primitiveAttachmentsTransient: [TwitterAttachment]? // sourcery: autoUpdatableObject - @objc public var attachments: [TwitterAttachment] { + @objc public private(set) var attachmentsTransient: [TwitterAttachment] { get { - let keyPath = #keyPath(TwitterStatus.attachments) + let keyPath = #keyPath(attachmentsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let attachments = primitiveAttachmentsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let attachments = try JSONDecoder().decode([TwitterAttachment].self, from: data) + if let attachments = attachments { return attachments - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.attachments + guard let data = _data else { + primitiveAttachmentsTransient = [] + return [] + } + let attachments = try JSONDecoder().decode([TwitterAttachment].self, from: data) + primitiveAttachmentsTransient = attachments + return attachments + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(TwitterStatus.attachments) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(attachmentsTransient) + do { + let data = try JSONEncoder().encode(newValue) + willChangeValue(forKey: keyPath) + primitiveAttachmentsTransient = newValue + didChangeValue(forKey: keyPath) + attachments = data + } catch { + assertionFailure() + } } } + @NSManaged private var location: Data? + @NSManaged private var primitiveLocationTransient: TwitterLocation? // sourcery: autoUpdatableObject - @objc public var location: TwitterLocation? { + @objc public private(set) var locationTransient: TwitterLocation? { get { - let keyPath = #keyPath(TwitterStatus.location) + let keyPath = #keyPath(locationTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let location = primitiveLocationTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let location = try JSONDecoder().decode(TwitterLocation.self, from: data) + if let location = location { return location - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.location + guard let data = _data else { + primitiveLocationTransient = nil + return nil + } + let location = try JSONDecoder().decode(TwitterLocation.self, from: data) + primitiveLocationTransient = location + return location + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.location) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(locationTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveLocationTransient = newValue + didChangeValue(forKey: keyPath) + location = data + } catch { + assertionFailure() + } } } + @NSManaged private var entities: Data? + @NSManaged private var primitiveEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var entities: TwitterEntity? { + @objc public private(set) var entitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterStatus.entities) + let keyPath = #keyPath(entitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let entities = primitiveEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + if let entities = entities { return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.entities + guard let data = _data else { + primitiveEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.entities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(entitiesTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + entities = data + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } + @NSManaged private var replySettings: Data? + @NSManaged private var primitiveReplySettingsTransient: TwitterReplySettings? // sourcery: autoUpdatableObject - @objc public var replySettings: TwitterReplySettings? { + @objc public private(set) var replySettingsTransient: TwitterReplySettings? { get { - let keyPath = #keyPath(TwitterStatus.replySettings) + let keyPath = #keyPath(replySettingsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let replySettings = primitiveReplySettingsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let replySettings = try JSONDecoder().decode(TwitterReplySettings.self, from: data) + if let replySettings = replySettings { return replySettings - } catch { - assertionFailure(error.localizedDescription) - return nil + } else { + do { + let _data = self.replySettings + guard let data = _data else { + primitiveReplySettingsTransient = nil + return nil + } + let replySettings = try JSONDecoder().decode(TwitterReplySettings.self, from: data) + primitiveReplySettingsTransient = replySettings + return replySettings + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterStatus.replySettings) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(replySettingsTransient) + do { + let data = try newValue.flatMap { try JSONEncoder().encode($0) } + willChangeValue(forKey: keyPath) + primitiveReplySettingsTransient = newValue + didChangeValue(forKey: keyPath) + replySettings = data + } catch { + assertionFailure() + } } } } @@ -412,24 +472,24 @@ extension TwitterStatus: AutoUpdatableObject { self.replyTo = replyTo } } - public func update(attachments: [TwitterAttachment]) { - if self.attachments != attachments { - self.attachments = attachments + public func update(attachmentsTransient: [TwitterAttachment]) { + if self.attachmentsTransient != attachmentsTransient { + self.attachmentsTransient = attachmentsTransient } } - public func update(location: TwitterLocation?) { - if self.location != location { - self.location = location + public func update(locationTransient: TwitterLocation?) { + if self.locationTransient != locationTransient { + self.locationTransient = locationTransient } } - public func update(entities: TwitterEntity?) { - if self.entities != entities { - self.entities = entities + public func update(entitiesTransient: TwitterEntity?) { + if self.entitiesTransient != entitiesTransient { + self.entitiesTransient = entitiesTransient } } - public func update(replySettings: TwitterReplySettings?) { - if self.replySettings != replySettings { - self.replySettings = replySettings + public func update(replySettingsTransient: TwitterReplySettings?) { + if self.replySettingsTransient != replySettingsTransient { + self.replySettingsTransient = replySettingsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index b350b999..90197b0a 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -16,7 +16,7 @@ extension TwitterStatus { public var displayText: String { var text = self.text - for url in entities?.urls ?? [] { + for url in entitiesTransient?.urls ?? [] { let shortURL = url.url guard let displayURL = url.displayURL, let expandedURL = url.expandedURL @@ -43,7 +43,7 @@ extension TwitterStatus { } public var urlEntities: [TwitterContent.URLEntity] { - let results = entities?.urls?.map { entity in + let results = entitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] @@ -53,7 +53,7 @@ extension TwitterStatus { extension TwitterStatus { /// The tweet more then 240 characters public var hasMore: Bool { - for url in entities?.urls ?? [] { + for url in entitiesTransient?.urls ?? [] { guard text.localizedCaseInsensitiveContains("… " + url.url) else { continue } guard let expandedURL = url.expandedURL else { continue } guard expandedURL.hasPrefix("https://twitter.com/") else { continue } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index e341565d..da28157b 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -65,7 +65,7 @@ extension StatusObject { switch self { case .twitter(let status): let status = status.repost ?? status - return status.attachments.map { .twitter($0) } + return status.attachmentsTransient.map { .twitter($0) } case .mastodon(let status): return status.attachments.map { .mastodon($0) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift index fab0c575..b46b6e84 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus+V2.swift @@ -274,14 +274,14 @@ extension Persistence.TwitterStatus { context: PersistContextV2 ) { // entities - status.update(entities: TwitterEntity(entity: context.entity.status.entities)) + status.update(entitiesTransient: TwitterEntity(entity: context.entity.status.entities)) // replySettings (v2 only) let _replySettings = context.entity.status.replySettings.flatMap { TwitterReplySettings(value: $0.rawValue) } if let replySettings = _replySettings { - status.update(replySettings: replySettings) + status.update(replySettingsTransient: replySettings) } // conversationID (v2 only) @@ -301,20 +301,20 @@ extension Persistence.TwitterStatus { // do not update video & GIFV attachments except isEmpty let isVideo = media.contains(where: { $0.type == TwitterAttachment.Kind.animatedGIF.rawValue || $0.type == TwitterAttachment.Kind.video.rawValue }) if isVideo { - let isEmpty = status.attachments.isEmpty + let isEmpty = status.attachmentsTransient.isEmpty if !isEmpty { return } } let attachments = media.compactMap { $0.twitterAttachment } - status.update(attachments: attachments) + status.update(attachmentsTransient: attachments) } // place (not stable: geo may erased) context.dictionary.place(for: context.entity.status) .flatMap { place in - status.update(location: place.twitterLocation) + status.update(locationTransient: place.twitterLocation) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift index f5c40b13..4761af5c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterStatus.swift @@ -203,15 +203,15 @@ extension Persistence.TwitterStatus { context: PersistContext ) { // prefer use V2 entities. only update entities when not exist - if status.entities == nil { - status.update(entities: TwitterEntity( + if status.entitiesTransient == nil { + status.update(entitiesTransient: TwitterEntity( entity: context.entity.entities, extendedEntity: context.entity.extendedEntities )) } - context.entity.twitterAttachments.flatMap { status.update(attachments: $0) } - context.entity.twitterLocation.flatMap { status.update(location:$0) } + context.entity.twitterAttachments.flatMap { status.update(attachmentsTransient: $0) } + context.entity.twitterLocation.flatMap { status.update(locationTransient: $0) } // update relationship if let me = context.me { diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 931df219..3254e529 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -251,7 +251,7 @@ extension MediaView.ViewModel { } public static func viewModels(from status: TwitterStatus) -> [MediaView.ViewModel] { - return status.attachments.map { attachment -> MediaView.ViewModel in + return status.attachmentsTransient.map { attachment -> MediaView.ViewModel in MediaView.ViewModel( mediaKind: { switch attachment.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 6357e160..c02f4510 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1100,7 +1100,7 @@ extension StatusView.ViewModel { } // reply settings - replySettingBannerViewModel = status.replySettings + replySettingBannerViewModel = status.replySettingsTransient .flatMap { object in Twitter.Entity.V2.Tweet.ReplySettings(rawValue: object.value) } .flatMap { replaySettings in ReplySettingBannerView.ViewModel( @@ -1171,7 +1171,7 @@ extension StatusView.ViewModel { } // location - location = status.location?.fullName + location = status.locationTransient?.fullName // metric switch kind { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9d0b1609..2d999258 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -213,7 +213,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { ) self.secondaryMentionPickItems = { var items: [MentionPickViewModel.Item] = [] - for mention in status.entities?.mentions ?? [] { + for mention in status.entitiesTransient?.mentions ?? [] { let username = mention.username let item = MentionPickViewModel.Item.twitterUser( username: username, @@ -376,7 +376,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { } excludeUsernames.insert(author.username) - for mention in status.entities?.mentions ?? [] { + for mention in status.entitiesTransient?.mentions ?? [] { guard !excludeUsernames.contains(mention.username) else { continue } usernames.append(mention.username) } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift index f413c261..75269411 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift @@ -72,7 +72,7 @@ extension DataSourceFacade { switch object { case .twitter(let status): let status = status.repost ?? status - let mentions = status.entities?.mentions ?? [] + let mentions = status.entitiesTransient?.mentions ?? [] let _userID: TwitterUser.ID? = mentions.first(where: { $0.username == mention })?.id if let userID = _userID { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index b878b79f..44917b6b 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -335,13 +335,13 @@ extension HomeTimelineViewController { case .quote: return status.quote != nil case .gif: - return status.attachments.contains(where: { attachment in attachment.kind == .animatedGIF }) + return status.attachmentsTransient.contains(where: { attachment in attachment.kind == .animatedGIF }) case .video: - return status.attachments.contains(where: { attachment in attachment.kind == .video }) + return status.attachmentsTransient.contains(where: { attachment in attachment.kind == .video }) case .poll: return status.poll != nil case .location: - return status.location != nil + return status.locationTransient != nil case .followsYouAuthor: guard case let .twitter(authenticationContext) = authenticationContext else { return false } guard let me = authenticationContext.authenticationRecord.object(in: AppContext.shared.managedObjectContext)?.user else { return false } From ee6b1a0e1b25466f8e6ba48af6e4754969a263a3 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 16 May 2023 18:27:40 +0800 Subject: [PATCH 07/17] feat: make others entity object's JSON data access via transient --- .../CoreDataStack 8.xcdatamodel/contents | 8 + .../Entity/Mastodon/MastodonList.swift | 26 --- .../MastodonNotificationSubscription.swift | 60 ++++-- .../Entity/Mastodon/MastodonStatus.swift | 194 ++++++++++++------ .../Entity/Mastodon/MastodonUser.swift | 128 ++++++++---- .../Entity/Twitter/TwitterStatus.swift | 22 +- .../Entity/Twitter/TwitterUser.swift | 116 +++++++---- .../AutoGenerateProperty.generated.swift | 36 ++++ .../AutoGenerateRelationship.generated.swift | 25 +++ .../AutoUpdatableObject.generated.swift | 24 +++ .../CoreDataStack/MastodonUser.swift | 4 +- .../Extension/CoreDataStack/TwitterUser.swift | 4 +- .../Notification/NotificationHeaderInfo.swift | 2 +- .../Model/Status/StatusObject.swift | 4 +- .../Extension/MastodonStatus+Property.swift | 6 +- .../Extension/MastodonUser+Property.swift | 4 +- .../Twitter/Persistence+TwitterUser+V2.swift | 4 +- .../Twitter/Persistence+TwitterUser.swift | 4 +- ...cationService+NotificationSubscriber.swift | 2 +- .../Content/MediaView+ViewModel.swift | 2 +- .../TwidereUI/Content/PollOptionView.swift | 2 +- .../Content/StatusView+ViewModel.swift | 6 +- .../ComposeContentViewModel.swift | 2 +- .../Facade/DataSourceFacade+Profile.swift | 4 +- .../View/ProfileHeaderView+ViewModel.swift | 13 +- .../MastodonNotificationSectionView.swift | 2 +- ...MastodonNotificationSectionViewModel.swift | 4 +- 27 files changed, 464 insertions(+), 244 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift create mode 100644 TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents index 1459ebff..7e268763 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 8.xcdatamodel/contents @@ -80,6 +80,7 @@ + @@ -119,10 +120,12 @@ + + @@ -130,6 +133,7 @@ + @@ -160,7 +164,9 @@ + + @@ -286,6 +292,7 @@ + @@ -301,6 +308,7 @@ + diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift index 46978fcb..fb0d8e17 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonList.swift @@ -169,30 +169,4 @@ extension MastodonList: AutoUpdatableObject { } } // sourcery:end - -// public func update(`private`: Bool) { -// if self.`private` != `private` { -// self.`private` = `private` -// } -// } -// public func update(memberCount: Int64) { -// if self.memberCount != memberCount { -// self.memberCount = memberCount -// } -// } -// public func update(followerCount: Int64) { -// if self.followerCount != followerCount { -// self.followerCount = followerCount -// } -// } -// public func update(theDescription: String?) { -// if self.theDescription != theDescription { -// self.theDescription = theDescription -// } -// } -// public func update(createdAt: Date?) { -// if self.createdAt != createdAt { -// self.createdAt = createdAt -// } -// } } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift index 1f1c50a1..120ca42f 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift @@ -48,28 +48,44 @@ final public class MastodonNotificationSubscription: NSManagedObject { } extension MastodonNotificationSubscription { + @NSManaged private var mentionPreference: Data? + @NSManaged private var primitiveMentionPreferenceTransient: MentionPreference? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var mentionPreference: MentionPreference { + @objc public private(set) var mentionPreferenceTransient: MentionPreference { get { - let keyPath = #keyPath(MastodonNotificationSubscription.mentionPreference) + let keyPath = #keyPath(mentionPreferenceTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let mentionPreference = primitiveMentionPreferenceTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data, !data.isEmpty else { return MentionPreference() } - let mentionPreference = try JSONDecoder().decode(MentionPreference.self, from: data) + if let mentionPreference = mentionPreference { return mentionPreference - } catch { - assertionFailure(error.localizedDescription) - return MentionPreference() + } else { + do { + let _data = self.mentionPreference + guard let data = _data, !data.isEmpty else { + primitiveMentionPreferenceTransient = MentionPreference() + return MentionPreference() + } + let mentionPreference = try JSONDecoder().decode(MentionPreference.self, from: data) + primitiveMentionPreferenceTransient = mentionPreference + return mentionPreference + } catch { + assertionFailure(error.localizedDescription) + return MentionPreference() + } } } set { - let keyPath = #keyPath(MastodonNotificationSubscription.mentionPreference) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(mentionPreferenceTransient) + do { + let data = try JSONEncoder().encode(newValue) + mentionPreference = data + willChangeValue(forKey: keyPath) + primitiveMentionPreferenceTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -124,7 +140,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { public let poll: Bool public let createdAt: Date public let updatedAt: Date - public let mentionPreference: MentionPreference + public let mentionPreferenceTransient: MentionPreference public init( id: ID?, @@ -140,7 +156,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { poll: Bool, createdAt: Date, updatedAt: Date, - mentionPreference: MentionPreference + mentionPreferenceTransient: MentionPreference ) { self.id = id self.domain = domain @@ -155,7 +171,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { self.poll = poll self.createdAt = createdAt self.updatedAt = updatedAt - self.mentionPreference = mentionPreference + self.mentionPreferenceTransient = mentionPreferenceTransient } } @@ -173,7 +189,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { self.poll = property.poll self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.mentionPreference = property.mentionPreference + self.mentionPreferenceTransient = property.mentionPreferenceTransient } public func update(property: Property) { @@ -190,7 +206,7 @@ extension MastodonNotificationSubscription: AutoGenerateProperty { update(poll: property.poll) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(mentionPreference: property.mentionPreference) + update(mentionPreferenceTransient: property.mentionPreferenceTransient) } // sourcery:end @@ -290,9 +306,9 @@ extension MastodonNotificationSubscription: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(mentionPreference: MentionPreference) { - if self.mentionPreference != mentionPreference { - self.mentionPreference = mentionPreference + public func update(mentionPreferenceTransient: MentionPreference) { + if self.mentionPreferenceTransient != mentionPreferenceTransient { + self.mentionPreferenceTransient = mentionPreferenceTransient } } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift index 41f48716..953d87e9 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift @@ -94,78 +94,138 @@ final public class MastodonStatus: NSManagedObject { } extension MastodonStatus { + @NSManaged private var attachments: Data? + @NSManaged private var primitiveAttachmentsTransient: [MastodonAttachment]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var attachments: [MastodonAttachment] { + @objc public private(set) var attachmentsTransient: [MastodonAttachment] { get { - let keyPath = #keyPath(MastodonStatus.attachments) + let keyPath = #keyPath(attachmentsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let attachments = primitiveAttachmentsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data, !data.isEmpty else { return [] } - let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data) + if let attachments = attachments { return attachments - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.attachments + guard let data = _data, !data.isEmpty else { + primitiveAttachmentsTransient = [] + return [] + } + let attachments = try JSONDecoder().decode([MastodonAttachment].self, from: data) + primitiveAttachmentsTransient = attachments + return attachments + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.attachments) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(attachmentsTransient) + do { + if newValue.isEmpty { + attachments = nil + } else { + let data = try JSONEncoder().encode(newValue) + attachments = data + } + willChangeValue(forKey: keyPath) + primitiveAttachmentsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var emojis: Data? + @NSManaged private var primitiveEmojisTransient: [MastodonEmoji]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var emojis: [MastodonEmoji] { + @objc public private(set) var emojisTransient: [MastodonEmoji] { get { - let keyPath = #keyPath(MastodonStatus.emojis) + let keyPath = #keyPath(emojisTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let emojis = primitiveEmojisTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + if let emojis = emojis { return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.emojis + guard let data = _data, !data.isEmpty else { + primitiveEmojisTransient = [] + return [] + } + let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + primitiveEmojisTransient = emojis + return emojis + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.emojis) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(emojisTransient) + do { + if newValue.isEmpty { + emojis = nil + } else { + let data = try JSONEncoder().encode(newValue) + emojis = data + } + willChangeValue(forKey: keyPath) + primitiveEmojisTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var mentions: Data? + @NSManaged private var primitiveMentionsTransient: [MastodonMention]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var mentions: [MastodonMention] { + @objc public private(set) var mentionsTransient: [MastodonMention] { get { - let keyPath = #keyPath(MastodonStatus.mentions) + let keyPath = #keyPath(mentionsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let mentions = primitiveMentionsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonMention].self, from: data) - return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + if let mentions = mentions { + return mentions + } else { + do { + let _data = self.mentions + guard let data = _data, !data.isEmpty else { + primitiveMentionsTransient = [] + return [] + } + let mentions = try JSONDecoder().decode([MastodonMention].self, from: data) + primitiveMentionsTransient = mentions + return mentions + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonStatus.mentions) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(mentionsTransient) + do { + if newValue.isEmpty { + mentions = nil + } else { + let data = try JSONEncoder().encode(newValue) + mentions = data + } + willChangeValue(forKey: keyPath) + primitiveMentionsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -249,9 +309,9 @@ extension MastodonStatus: AutoGenerateProperty { public let replyToUserID: MastodonStatus.ID? public let createdAt: Date public let updatedAt: Date - public let attachments: [MastodonAttachment] - public let emojis: [MastodonEmoji] - public let mentions: [MastodonMention] + public let attachmentsTransient: [MastodonAttachment] + public let emojisTransient: [MastodonEmoji] + public let mentionsTransient: [MastodonMention] public init( id: ID, @@ -272,9 +332,9 @@ extension MastodonStatus: AutoGenerateProperty { replyToUserID: MastodonStatus.ID?, createdAt: Date, updatedAt: Date, - attachments: [MastodonAttachment], - emojis: [MastodonEmoji], - mentions: [MastodonMention] + attachmentsTransient: [MastodonAttachment], + emojisTransient: [MastodonEmoji], + mentionsTransient: [MastodonMention] ) { self.id = id self.domain = domain @@ -294,9 +354,9 @@ extension MastodonStatus: AutoGenerateProperty { self.replyToUserID = replyToUserID self.createdAt = createdAt self.updatedAt = updatedAt - self.attachments = attachments - self.emojis = emojis - self.mentions = mentions + self.attachmentsTransient = attachmentsTransient + self.emojisTransient = emojisTransient + self.mentionsTransient = mentionsTransient } } @@ -319,9 +379,9 @@ extension MastodonStatus: AutoGenerateProperty { self.replyToUserID = property.replyToUserID self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.attachments = property.attachments - self.emojis = property.emojis - self.mentions = property.mentions + self.attachmentsTransient = property.attachmentsTransient + self.emojisTransient = property.emojisTransient + self.mentionsTransient = property.mentionsTransient } public func update(property: Property) { @@ -340,9 +400,9 @@ extension MastodonStatus: AutoGenerateProperty { update(replyToUserID: property.replyToUserID) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(attachments: property.attachments) - update(emojis: property.emojis) - update(mentions: property.mentions) + update(attachmentsTransient: property.attachmentsTransient) + update(emojisTransient: property.emojisTransient) + update(mentionsTransient: property.mentionsTransient) } // sourcery:end } @@ -468,19 +528,19 @@ extension MastodonStatus: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(attachments: [MastodonAttachment]) { - if self.attachments != attachments { - self.attachments = attachments + public func update(attachmentsTransient: [MastodonAttachment]) { + if self.attachmentsTransient != attachmentsTransient { + self.attachmentsTransient = attachmentsTransient } } - public func update(emojis: [MastodonEmoji]) { - if self.emojis != emojis { - self.emojis = emojis + public func update(emojisTransient: [MastodonEmoji]) { + if self.emojisTransient != emojisTransient { + self.emojisTransient = emojisTransient } } - public func update(mentions: [MastodonMention]) { - if self.mentions != mentions { - self.mentions = mentions + public func update(mentionsTransient: [MastodonMention]) { + if self.mentionsTransient != mentionsTransient { + self.mentionsTransient = mentionsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift index 3940e6c9..1bcc78fc 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonUser.swift @@ -97,53 +97,93 @@ final public class MastodonUser: NSManagedObject { } extension MastodonUser { + @NSManaged private var emojis: Data? + @NSManaged private var primitiveEmojisTransient: [MastodonEmoji]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var emojis: [MastodonEmoji] { + @objc public private(set) var emojisTransient: [MastodonEmoji] { get { - let keyPath = #keyPath(MastodonUser.emojis) + let keyPath = #keyPath(emojisTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let emojis = primitiveEmojisTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + if let emojis = emojis { return emojis - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.emojis + guard let data = _data, !data.isEmpty else { + primitiveEmojisTransient = [] + return [] + } + let emojis = try JSONDecoder().decode([MastodonEmoji].self, from: data) + primitiveEmojisTransient = emojis + return emojis + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonUser.emojis) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(emojisTransient) + do { + if newValue.isEmpty { + emojis = nil + } else { + let data = try JSONEncoder().encode(newValue) + emojis = data + } + willChangeValue(forKey: keyPath) + primitiveEmojisTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } + @NSManaged private var fields: Data? + @NSManaged private var primitiveFieldsTransient: [MastodonField]? // sourcery: autoUpdatableObject, autoGenerateProperty - @objc public var fields: [MastodonField] { + @objc public private(set) var fieldsTransient: [MastodonField] { get { - let keyPath = #keyPath(MastodonUser.fields) + let keyPath = #keyPath(fieldsTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let fields = primitiveFieldsTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return [] } - let fields = try JSONDecoder().decode([MastodonField].self, from: data) + if let fields = fields { return fields - } catch { - assertionFailure(error.localizedDescription) - return [] + } else { + do { + let _data = self.fields + guard let data = _data, !data.isEmpty else { + primitiveFieldsTransient = [] + return [] + } + let fields = try JSONDecoder().decode([MastodonField].self, from: data) + primitiveFieldsTransient = fields + return fields + } catch { + assertionFailure(error.localizedDescription) + return [] + } } } set { - let keyPath = #keyPath(MastodonUser.fields) - let data = try? JSONEncoder().encode(newValue) - willChangeValue(forKey: keyPath) - setPrimitiveValue(data, forKey: keyPath) - didChangeValue(forKey: keyPath) + let keyPath = #keyPath(fieldsTransient) + do { + if newValue.isEmpty { + fields = nil + } else { + let data = try JSONEncoder().encode(newValue) + fields = data + } + willChangeValue(forKey: keyPath) + primitiveFieldsTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() + } } } } @@ -235,8 +275,8 @@ extension MastodonUser: AutoGenerateProperty { public let suspended: Bool public let createdAt: Date public let updatedAt: Date - public let emojis: [MastodonEmoji] - public let fields: [MastodonField] + public let emojisTransient: [MastodonEmoji] + public let fieldsTransient: [MastodonField] public init( domain: String, @@ -258,8 +298,8 @@ extension MastodonUser: AutoGenerateProperty { suspended: Bool, createdAt: Date, updatedAt: Date, - emojis: [MastodonEmoji], - fields: [MastodonField] + emojisTransient: [MastodonEmoji], + fieldsTransient: [MastodonField] ) { self.domain = domain self.id = id @@ -280,8 +320,8 @@ extension MastodonUser: AutoGenerateProperty { self.suspended = suspended self.createdAt = createdAt self.updatedAt = updatedAt - self.emojis = emojis - self.fields = fields + self.emojisTransient = emojisTransient + self.fieldsTransient = fieldsTransient } } @@ -305,8 +345,8 @@ extension MastodonUser: AutoGenerateProperty { self.suspended = property.suspended self.createdAt = property.createdAt self.updatedAt = property.updatedAt - self.emojis = property.emojis - self.fields = property.fields + self.emojisTransient = property.emojisTransient + self.fieldsTransient = property.fieldsTransient } public func update(property: Property) { @@ -327,8 +367,8 @@ extension MastodonUser: AutoGenerateProperty { update(suspended: property.suspended) update(createdAt: property.createdAt) update(updatedAt: property.updatedAt) - update(emojis: property.emojis) - update(fields: property.fields) + update(emojisTransient: property.emojisTransient) + update(fieldsTransient: property.fieldsTransient) } // sourcery:end } @@ -424,14 +464,14 @@ extension MastodonUser: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(emojis: [MastodonEmoji]) { - if self.emojis != emojis { - self.emojis = emojis + public func update(emojisTransient: [MastodonEmoji]) { + if self.emojisTransient != emojisTransient { + self.emojisTransient = emojisTransient } } - public func update(fields: [MastodonField]) { - if self.fields != fields { - self.fields = fields + public func update(fieldsTransient: [MastodonField]) { + if self.fieldsTransient != fieldsTransient { + self.fieldsTransient = fieldsTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index dbc50a0f..e83d7811 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -97,7 +97,7 @@ extension TwitterStatus { } else { do { let _data = self.attachments - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveAttachmentsTransient = [] return [] } @@ -113,11 +113,15 @@ extension TwitterStatus { set { let keyPath = #keyPath(attachmentsTransient) do { - let data = try JSONEncoder().encode(newValue) + if newValue.isEmpty { + attachments = nil + } else { + let data = try JSONEncoder().encode(newValue) + attachments = data + } willChangeValue(forKey: keyPath) primitiveAttachmentsTransient = newValue didChangeValue(forKey: keyPath) - attachments = data } catch { assertionFailure() } @@ -138,7 +142,7 @@ extension TwitterStatus { } else { do { let _data = self.location - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveLocationTransient = nil return nil } @@ -155,10 +159,10 @@ extension TwitterStatus { let keyPath = #keyPath(locationTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + location = data willChangeValue(forKey: keyPath) primitiveLocationTransient = newValue didChangeValue(forKey: keyPath) - location = data } catch { assertionFailure() } @@ -179,7 +183,7 @@ extension TwitterStatus { } else { do { let _data = self.entities - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveEntitiesTransient = nil return nil } @@ -196,10 +200,10 @@ extension TwitterStatus { let keyPath = #keyPath(entitiesTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + entities = data willChangeValue(forKey: keyPath) primitiveEntitiesTransient = newValue didChangeValue(forKey: keyPath) - entities = data } catch { assertionFailure() } @@ -220,7 +224,7 @@ extension TwitterStatus { } else { do { let _data = self.replySettings - guard let data = _data else { + guard let data = _data, !data.isEmpty else { primitiveReplySettingsTransient = nil return nil } @@ -237,10 +241,10 @@ extension TwitterStatus { let keyPath = #keyPath(replySettingsTransient) do { let data = try newValue.flatMap { try JSONEncoder().encode($0) } + replySettings = data willChangeValue(forKey: keyPath) primitiveReplySettingsTransient = newValue didChangeValue(forKey: keyPath) - replySettings = data } catch { assertionFailure() } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift index 0627bdf7..c5a5fb13 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterUser.swift @@ -79,61 +79,93 @@ public final class TwitterUser: NSManagedObject { } extension TwitterUser { + @NSManaged private var bioEntities: Data? + @NSManaged private var primitiveBioEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var bioEntities: TwitterEntity? { + @objc public private(set) var bioEntitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterUser.bioEntities) + let keyPath = #keyPath(bioEntitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let bioEntities = primitiveBioEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) - return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + if let bioEntities = bioEntities { + return bioEntities + } else { + do { + let _data = self.bioEntities + guard let data = _data, !data.isEmpty else { + primitiveBioEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveBioEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterUser.bioEntities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(bioEntitiesTransient) + do { + if let newValue = newValue { + let data = try JSONEncoder().encode(newValue) + bioEntities = data + } else { + bioEntities = nil + } + willChangeValue(forKey: keyPath) + primitiveBioEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } + @NSManaged private var urlEntities: Data? + @NSManaged private var primitiveUrlEntitiesTransient: TwitterEntity? // sourcery: autoUpdatableObject - @objc public var urlEntities: TwitterEntity? { + @objc public private(set) var urlEntitiesTransient: TwitterEntity? { get { - let keyPath = #keyPath(TwitterUser.urlEntities) + let keyPath = #keyPath(urlEntitiesTransient) willAccessValue(forKey: keyPath) - let _data = primitiveValue(forKey: keyPath) as? Data + let urlEntities = primitiveUrlEntitiesTransient didAccessValue(forKey: keyPath) - do { - guard let data = _data else { return nil } - let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) - return entities - } catch { - assertionFailure(error.localizedDescription) - return nil + if let urlEntities = urlEntities { + return urlEntities + } else { + do { + let _data = self.urlEntities + guard let data = _data, !data.isEmpty else { + primitiveUrlEntitiesTransient = nil + return nil + } + let entities = try JSONDecoder().decode(TwitterEntity.self, from: data) + primitiveUrlEntitiesTransient = entities + return entities + } catch { + assertionFailure(error.localizedDescription) + return nil + } } } set { - let keyPath = #keyPath(TwitterUser.urlEntities) - willChangeValue(forKey: keyPath) - if let newValue = newValue { - let data = try? JSONEncoder().encode(newValue) - setPrimitiveValue(data, forKey: keyPath) - } else { - setPrimitiveValue(nil, forKey: keyPath) + let keyPath = #keyPath(urlEntitiesTransient) + do { + if let newValue = newValue { + let data = try JSONEncoder().encode(newValue) + urlEntities = data + } else { + urlEntities = nil + } + willChangeValue(forKey: keyPath) + primitiveUrlEntitiesTransient = newValue + didChangeValue(forKey: keyPath) + } catch { + assertionFailure() } - didChangeValue(forKey: keyPath) } } } @@ -357,14 +389,14 @@ extension TwitterUser: AutoUpdatableObject { self.updatedAt = updatedAt } } - public func update(bioEntities: TwitterEntity?) { - if self.bioEntities != bioEntities { - self.bioEntities = bioEntities + public func update(bioEntitiesTransient: TwitterEntity?) { + if self.bioEntitiesTransient != bioEntitiesTransient { + self.bioEntitiesTransient = bioEntitiesTransient } } - public func update(urlEntities: TwitterEntity?) { - if self.urlEntities != urlEntities { - self.urlEntities = urlEntities + public func update(urlEntitiesTransient: TwitterEntity?) { + if self.urlEntitiesTransient != urlEntitiesTransient { + self.urlEntitiesTransient = urlEntitiesTransient } } // sourcery:end diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift new file mode 100644 index 00000000..eed4b7e0 --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateProperty.generated.swift @@ -0,0 +1,36 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + + + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoGenerateProperty + +// Generated using Sourcery +// DO NOT EDIT +public struct Property { + + public init( + ) { + } +} + +public func configure(property: Property) { +} + +public func update(property: Property) { +} +// sourcery:end + + + + + + + + diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift new file mode 100644 index 00000000..29fddaeb --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoGenerateRelationship.generated.swift @@ -0,0 +1,25 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoGenerateRelationship + +// Generated using Sourcery +// DO NOT EDIT +public struct Relationship { + + public init( + ) { + } +} + +public func configure(relationship: Relationship) { +} + +// sourcery:end + + + + diff --git a/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift b/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift new file mode 100644 index 00000000..48c23e3d --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Generated/AutoUpdatableObject.generated.swift @@ -0,0 +1,24 @@ +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + + + + + + + + +// sourcery:inline:MastodonStatus.MastodonStatus.AutoUpdatableObject + +// Generated using Sourcery +// DO NOT EDIT +// sourcery:end + + + + + + + + diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift index 1da6c9ca..383a4af6 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/MastodonUser.swift @@ -32,7 +32,7 @@ extension MastodonUser { extension MastodonUser { public var nameMetaContent: MastodonMetaContent? { do { - let content = MastodonContent(content: name, emojis: emojis.asDictionary) + let content = MastodonContent(content: name, emojis: emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { @@ -44,7 +44,7 @@ extension MastodonUser { public var bioMetaContent: MastodonMetaContent? { guard let note = note else { return nil } do { - let content = MastodonContent(content: note, emojis: emojis.asDictionary) + let content = MastodonContent(content: note, emojis: emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift index a6bcab08..ba4b022c 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift @@ -81,7 +81,7 @@ extension String { extension TwitterUser { public var bioURLEntities: [TwitterContent.URLEntity] { - let results = bioEntities?.urls?.map { entity in + let results = bioEntitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] @@ -99,7 +99,7 @@ extension TwitterUser { } public var urlEntity: [TwitterContent.URLEntity] { - let results = urlEntities?.urls?.map { entity in + let results = urlEntitiesTransient?.urls?.map { entity in TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) } return results ?? [] diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift index f7c05754..216b7ec6 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift @@ -101,7 +101,7 @@ extension NotificationHeaderInfo { // assertionFailure() return nil } - let content = MastodonContent(content: text, emojis: user.emojis.asDictionary) + let content = MastodonContent(content: text, emojis: user.emojisTransient.asDictionary) return Meta.convert(document: .mastodon(content: content)) } } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index da28157b..1cf6af9e 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -43,7 +43,7 @@ extension StatusObject { return status.displayText case .mastodon(let status): do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.content, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent.original } catch { @@ -67,7 +67,7 @@ extension StatusObject { let status = status.repost ?? status return status.attachmentsTransient.map { .twitter($0) } case .mastodon(let status): - return status.attachments.map { .mastodon($0) } + return status.attachmentsTransient.map { .mastodon($0) } } } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift index 0f651d20..764555f7 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonStatus+Property.swift @@ -37,9 +37,9 @@ extension MastodonStatus.Property { replyToUserID: entity.inReplyToAccountID, createdAt: entity.createdAt, updatedAt: networkDate, - attachments: entity.mastodonAttachments, - emojis: entity.mastodonEmojis, - mentions: entity.mastodonMentions + attachmentsTransient: entity.mastodonAttachments, + emojisTransient: entity.mastodonEmojis, + mentionsTransient: entity.mastodonMentions ) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift index 199e3bc5..55df28cf 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Mastodon/Extension/MastodonUser+Property.swift @@ -32,8 +32,8 @@ extension MastodonUser.Property { suspended: entity.suspended ?? false, createdAt: entity.createdAt, updatedAt: networkDate, - emojis: entity.mastodonEmojis, - fields: entity.mastodonFields + emojisTransient: entity.mastodonEmojis, + fieldsTransient: entity.mastodonFields ) } } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift index 5feac52a..4ace8978 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser+V2.swift @@ -102,8 +102,8 @@ extension Persistence.TwitterUser { twitterUser user: TwitterUser, context: PersistContextV2 ) { - user.update(bioEntities: TwitterEntity(entity: context.entity.entities?.description)) - user.update(urlEntities: TwitterEntity(entity: context.entity.entities?.url)) + user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) + user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) // V2 entity not contains relationship flags } diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift index df83510a..0c01df9c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterUser.swift @@ -115,8 +115,8 @@ extension Persistence.TwitterUser { context: PersistContext ) { user.update(profileBannerURL: context.entity.profileBannerURL) - user.update(bioEntities: TwitterEntity(entity: context.entity.entities?.description)) - user.update(urlEntities: TwitterEntity(entity: context.entity.entities?.url)) + user.update(bioEntitiesTransient: TwitterEntity(entity: context.entity.entities?.description)) + user.update(urlEntitiesTransient: TwitterEntity(entity: context.entity.entities?.url)) // relationship if let me = context.me { diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift index bc10c883..f0ed7187 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift @@ -204,7 +204,7 @@ extension NotificationService.MastodonNotificationSubscriber { poll: true, createdAt: now, updatedAt: now, - mentionPreference: MastodonNotificationSubscription.MentionPreference() + mentionPreferenceTransient: MastodonNotificationSubscription.MentionPreference() ), relationship: .init(authentication: authentication) ) diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift index 3254e529..be129b78 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView+ViewModel.swift @@ -271,7 +271,7 @@ extension MediaView.ViewModel { } public static func viewModels(from status: MastodonStatus) -> [MediaView.ViewModel] { - return status.attachments.map { attachment -> MediaView.ViewModel in + return status.attachmentsTransient.map { attachment -> MediaView.ViewModel in MediaView.ViewModel( mediaKind: { switch attachment.kind { diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index f942b359..d3af2b19 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -178,7 +178,7 @@ extension PollOptionView { index = Int(option.index) content = { do { - let content = MastodonContent(content: option.title, emojis: option.poll.status.emojis.asDictionary) + let content = MastodonContent(content: option.title, emojis: option.poll.status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content) return metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index c02f4510..5fdd7763 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -1262,7 +1262,7 @@ extension StatusView.ViewModel { label: { let name = status.author.name let userRepostText = L10n.Common.Controls.Status.userBoosted(name) - let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) + let text = MastodonContent(content: userRepostText, emojis: status.author.emojisTransient.asDictionary) let label = MastodonMetaContent.convert(text: text) return label }() @@ -1298,7 +1298,7 @@ extension StatusView.ViewModel { // spoiler content if let spoilerText = status.spoilerText, !spoilerText.isEmpty { do { - let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: spoilerText, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.spoilerContent = metaContent } catch { @@ -1309,7 +1309,7 @@ extension StatusView.ViewModel { // content do { - let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) + let content = MastodonContent(content: status.content, emojis: status.emojisTransient.asDictionary) let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) self.content = metaContent } catch { diff --git a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift index 2d999258..3704d244 100644 --- a/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -248,7 +248,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject { if _authorUserIdentifier?.id != status.author.id { mentionAccts.append("@" + status.author.acct) } - for mention in status.mentions { + for mention in status.mentionsTransient { let acct = "@" + mention.acct guard !mentionAccts.contains(acct) else { continue } guard mention.id != _authorUserIdentifier?.id else { continue } diff --git a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift index 75269411..f1ceef63 100644 --- a/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift +++ b/TwidereX/Protocol/Facade/DataSourceFacade+Profile.swift @@ -92,7 +92,7 @@ extension DataSourceFacade { case .mastodon(let status): let status = status.repost ?? status - guard let mention = status.mentions.first(where: { mention == $0.username }) else { + guard let mention = status.mentionsTransient.first(where: { mention == $0.username }) else { return nil } @@ -138,7 +138,7 @@ extension DataSourceFacade { guard let object = user.object(in: provider.context.managedObjectContext) else { return nil } switch object { case .twitter(let user): - let mentions = user.bioEntities?.mentions ?? [] + let mentions = user.bioEntitiesTransient?.mentions ?? [] let _userID = mentions.first(where: { $0.username == mention })?.id if let userID = _userID { diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index 0bde2fcd..972d7711 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -12,6 +12,7 @@ import CoreDataStack import TwitterMeta import MastodonMeta import AlamofireImage +import TwidereCore extension ProfileHeaderView { class ViewModel: ObservableObject { @@ -36,7 +37,7 @@ extension ProfileHeaderView { @Published var username: String = "" @Published var isFollowsYou: Bool = false - @Published var relationship: Relationship? + @Published var relationship: TwidereCore.Relationship? @Published var bioMetaContent: MetaContent? @Published var fields: [ProfileFieldListView.Item]? @@ -265,7 +266,7 @@ extension ProfileHeaderView { private func configureContent(twitterUser user: TwitterUser) { Publishers.CombineLatest3( user.publisher(for: \.bio), - user.publisher(for: \.bioEntities), + user.publisher(for: \.bioEntitiesTransient), UIContentSizeCategory.publisher ) .map { _, _, _ in user.bioMetaContent(provider: SwiftTwitterTextProvider()) } @@ -349,7 +350,7 @@ extension ProfileHeaderView { // name Publishers.CombineLatest3( user.publisher(for: \.displayName), - user.publisher(for: \.emojis), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { _, emojis, _ -> MetaContent in @@ -382,7 +383,7 @@ extension ProfileHeaderView { private func configureContent(mastodonUser user: MastodonUser) { Publishers.CombineLatest3( user.publisher(for: \.note), - user.publisher(for: \.emojis), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { _, _, _ in user.bioMetaContent } @@ -392,8 +393,8 @@ extension ProfileHeaderView { private func configureField(mastodonUser user: MastodonUser) { Publishers.CombineLatest3( - user.publisher(for: \.fields), - user.publisher(for: \.emojis), + user.publisher(for: \.fieldsTransient), + user.publisher(for: \.emojisTransient), UIContentSizeCategory.publisher ) .map { fields, emojis, _ -> [ProfileFieldListView.Item]? in diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift index 2fc5c9f8..ac6a588a 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift @@ -117,7 +117,7 @@ extension MastodonNotificationSectionView { viewModel.mentionPreference = newValue viewModel.updateNotificationSubscription { notificationSubscription in let mentionPreference = MastodonNotificationSubscription.MentionPreference(preference: newValue) - notificationSubscription.update(mentionPreference: mentionPreference) + notificationSubscription.update(mentionPreferenceTransient: mentionPreference) } } )) { diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift index fe351dcc..4ff5da7d 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift @@ -43,7 +43,7 @@ final class MastodonNotificationSectionViewModel: ObservableObject { self.isFavoriteEnabled = notificationSubscription.favourite self.isPollEnabled = notificationSubscription.poll self.isMentionEnabled = notificationSubscription.mention - self.mentionPreference = notificationSubscription.mentionPreference.preference + self.mentionPreference = notificationSubscription.mentionPreferenceTransient.preference // end init notificationSubscription.publisher(for: \.isActive) @@ -66,7 +66,7 @@ final class MastodonNotificationSectionViewModel: ObservableObject { .receive(on: DispatchQueue.main) .assign(to: &$isMentionEnabled) - notificationSubscription.publisher(for: \.mentionPreference) + notificationSubscription.publisher(for: \.mentionPreferenceTransient) .receive(on: DispatchQueue.main) .map { $0.preference } .assign(to: &$mentionPreference) From fa004c3497fc6b3d0d7b04e7034195fb407885a5 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 18 May 2023 19:49:09 +0800 Subject: [PATCH 08/17] feat: add new SwiftUI TrendView --- .../Extension/CoreDataStack/TwitterUser.swift | 1 - .../Sources/TwidereUI/Content/TrendView.swift | 89 +++++++++++++++---- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift index ba4b022c..5831ef39 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterUser.swift @@ -51,7 +51,6 @@ extension TwitterUser { } extension TwitterUser { - public enum SizeKind: String { case small case medium diff --git a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift index e6cad025..2eded997 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/TrendView.swift @@ -11,6 +11,7 @@ import Meta import MetaTextKit import TwitterSDK import MastodonSDK +import Charts public struct TrendView: View { @@ -21,15 +22,33 @@ public struct TrendView: View { } public var body: some View { - HStack { - VStack(alignment: .leading, spacing: .zero) { - titleLabel - descriptionLabel + switch viewModel.kind { + case .twitter: + HStack { + VStack(alignment: .leading, spacing: .zero) { + titleLabel + } + .alignmentGuide(.listRowSeparatorLeading) { viewDimensions in + viewDimensions[.leading] + } + Spacer() } - Spacer() + + case .mastodon: HStack { - chartDescriptionLabel - chartView + VStack(alignment: .leading, spacing: .zero) { + titleLabel + descriptionLabel + } + .alignmentGuide(.listRowSeparatorLeading) { viewDimensions in + viewDimensions[.leading] + } + Spacer() + HStack { + chartDescriptionLabel + chartView + .frame(width: 66, height: 40) + } } } } @@ -66,11 +85,41 @@ extension TrendView { .foregroundColor(.secondary) } + var chartLineGradient: LinearGradient { + LinearGradient( + gradient: Gradient ( + colors: [ + Color(uiColor: Asset.Colors.hightLight.color).opacity(0.5), + Color(uiColor: Asset.Colors.hightLight.color).opacity(0.0), + ] + ), + startPoint: .top, + endPoint: .bottom + ) + } + var chartView: some View { - EmptyView() -// Chart { -// -// } + Group { + if let historyData = viewModel.historyData { + Chart(historyData, id: \.self) { data in + LineMark( + x: .value("Day", data.day), + y: .value("Accounts", Int(data.accounts) ?? 0) + ) + .interpolationMethod(.catmullRom) + + AreaMark( + x: .value("Day", data.day), + y: .value("Accounts", Int(data.accounts) ?? 0) + ) + .interpolationMethod(.catmullRom) + .foregroundStyle(chartLineGradient) + } + .chartLegend(.hidden) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } + } // Group } } @@ -169,13 +218,13 @@ extension TrendView.ViewModel { ) historyData = [ - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "123"), - Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "123"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 1), uses: "123", accounts: "12"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 2), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 3), uses: "123", accounts: "33"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 4), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 5), uses: "123", accounts: "13"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 6), uses: "123", accounts: "23"), + Mastodon.Entity.History(day: Date(year: 2020, month: 5, day: 7), uses: "123", accounts: "53"), ] } } @@ -185,7 +234,11 @@ extension TrendView.ViewModel { struct TrendView_Previews: PreviewProvider { static var previews: some View { TrendView(viewModel: .init(kind: .twitter)) + .previewDisplayName("Twitter") + .previewLayout(.fixed(width: 475, height: 88)) TrendView(viewModel: .init(kind: .mastodon)) + .previewDisplayName("Mastodon") + .previewLayout(.fixed(width: 475, height: 88)) } } #endif From 181202f5d491b651b7c26cc44c67aaf25545c93d Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 18 May 2023 19:49:50 +0800 Subject: [PATCH 09/17] chore: update version to 2.0.0 (131) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 685ee726..a535b3bd 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 130 + 131 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 1d95e087..74c8e822 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 106f8f28..5d26665b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3431,7 +3431,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3458,7 +3458,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3481,7 +3481,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3505,7 +3505,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3532,7 +3532,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3561,7 +3561,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3590,7 +3590,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3618,7 +3618,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3644,7 +3644,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3671,7 +3671,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3825,7 +3825,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3857,7 +3857,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3884,7 +3884,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3909,7 +3909,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3932,7 +3932,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3955,7 +3955,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3979,7 +3979,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4006,7 +4006,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 131; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index ca828d48..b8160ed3 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 09edc087..f312c6c0 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 45d40d23..25b47773 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 45d40d23..25b47773 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 130 + 131 From f079d216638da222f067877d198036b6b63e70fb Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 23 May 2023 20:31:53 +0800 Subject: [PATCH 10/17] feat: add multi column for iPadOS --- .../Extension/NSLayoutConstraint.swift | 7 + .../Content/StatusView+ViewModel.swift | 1 + TwidereX.xcodeproj/project.pbxproj | 36 +++ TwidereX/Coordinator/SceneCoordinator.swift | 2 +- TwidereX/Diffable/Status/StatusSection.swift | 2 - ...HomeListStatusTimelineViewController.swift | 6 + .../Scene/Profile/ProfileViewController.swift | 6 + .../Root/ContentSplitViewController.swift | 41 +++- .../Scene/Root/NewColumn/NewColumnView.swift | 40 ++++ .../NewColumn/NewColumnViewController.swift | 55 +++++ .../Root/NewColumn/NewColumnViewModel.swift | 73 ++++++ .../SecondaryContainerViewController.swift | 224 ++++++++++++++++++ .../SecondaryContainerViewModel.swift | 146 ++++++++++++ .../Base/Common/TimelineViewController.swift | 6 + 14 files changed, 641 insertions(+), 4 deletions(-) create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnView.swift create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift create mode 100644 TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift create mode 100644 TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift create mode 100644 TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift diff --git a/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift b/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift index 60a169ae..10524dfb 100644 --- a/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift +++ b/TwidereSDK/Sources/TwidereCommon/Extension/NSLayoutConstraint.swift @@ -13,3 +13,10 @@ extension NSLayoutConstraint { return self } } + +extension NSLayoutConstraint { + public func identifier(_ identifier: String) -> Self { + self.identifier = identifier + return self + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 5fdd7763..ab809e0e 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -940,6 +940,7 @@ extension StatusView.ViewModel { case .timeline, .referenceReplyTo: width += StatusView.hangingAvatarButtonDimension width += StatusView.hangingAvatarButtonTrailingSpacing + width += 2 * 16 // cell margin default: break } diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 5d26665b..8486256b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */; }; DB02C777273520B8007EA0BF /* SearchHashtagViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */; }; DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */; }; + DB0455B62A1CA2F3009A00EF /* NewColumnViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */; }; + DB0455B82A1CA510009A00EF /* NewColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0455B72A1CA510009A00EF /* NewColumnView.swift */; }; DB05E13729C318530055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13629C318530055BF3F /* TwidereSDK */; }; DB05E13929C318590055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13829C318590055BF3F /* TwidereSDK */; }; DB05E13B29C3185E0055BF3F /* TwidereSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB05E13A29C3185E0055BF3F /* TwidereSDK */; }; @@ -331,6 +333,9 @@ DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */; }; DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */; }; DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */; }; + DBEA97632A1B556C00C8B75B /* SecondaryContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */; }; + DBEA97662A1B58E200C8B75B /* SecondaryContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */; }; + DBEA97682A1B6D2300C8B75B /* NewColumnViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */; }; DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3309025B96E0B00A678FB /* WKNavigationDelegateShim.swift */; }; @@ -487,6 +492,8 @@ DB02C77427351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB02C776273520B8007EA0BF /* SearchHashtagViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+State.swift"; sourceTree = ""; }; DB02C77827352217007EA0BF /* SearchHashtagViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchHashtagViewModel+Diffable.swift"; sourceTree = ""; }; + DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnViewModel.swift; sourceTree = ""; }; + DB0455B72A1CA510009A00EF /* NewColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnView.swift; sourceTree = ""; }; DB06180F2786EC870030EE79 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = ""; }; DB0AD4DE285872BE0002ABDB /* UserMediaTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMediaTimelineViewController.swift; sourceTree = ""; }; DB0AD4E12858734A0002ABDB /* UserMediaTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMediaTimelineViewModel.swift; sourceTree = ""; }; @@ -828,6 +835,9 @@ DBE76CF82500B29500DEB0FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DBE76CFE2500B43300DEB0FC /* StubMixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubMixer.swift; sourceTree = ""; }; DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = ""; }; + DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewController.swift; sourceTree = ""; }; + DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContainerViewModel.swift; sourceTree = ""; }; + DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewColumnViewController.swift; sourceTree = ""; }; DBED2A7125B8006400BE6941 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; DBED96D7253F5D7800C5383A /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DBEFDC8725591F5C0086F268 /* DrawerSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarViewController.swift; sourceTree = ""; }; @@ -1062,6 +1072,8 @@ DB148B01281A729600B596C7 /* Sidebar */, DBDA8E9624FE075E006750DC /* MainTab */, DB66DB8E2823A7CC0071F5F3 /* SecondaryTab */, + DBEA97642A1B557100C8B75B /* SecondaryContainer */, + DBEA97692A1B6D2C00C8B75B /* NewColumn */, DB148B02281A7AB300B596C7 /* ContentSplitViewController.swift */, ); path = Root; @@ -2245,6 +2257,25 @@ path = View; sourceTree = ""; }; + DBEA97642A1B557100C8B75B /* SecondaryContainer */ = { + isa = PBXGroup; + children = ( + DBEA97622A1B556C00C8B75B /* SecondaryContainerViewController.swift */, + DBEA97652A1B58E200C8B75B /* SecondaryContainerViewModel.swift */, + ); + path = SecondaryContainer; + sourceTree = ""; + }; + DBEA97692A1B6D2C00C8B75B /* NewColumn */ = { + isa = PBXGroup; + children = ( + DBEA97672A1B6D2300C8B75B /* NewColumnViewController.swift */, + DB0455B52A1CA2F3009A00EF /* NewColumnViewModel.swift */, + DB0455B72A1CA510009A00EF /* NewColumnView.swift */, + ); + path = NewColumn; + sourceTree = ""; + }; DBED96DC253F5D7B00C5383A /* Protocol */ = { isa = PBXGroup; children = ( @@ -2888,6 +2919,7 @@ DBA7DD74256B96450008A95A /* UIFont.swift in Sources */, DB1D3DF228938CDF008F0BD0 /* StatusHistoryViewModel.swift in Sources */, DB76A652275F1C3200A50673 /* MediaPreviewTransitionItem.swift in Sources */, + DBEA97632A1B556C00C8B75B /* SecondaryContainerViewController.swift in Sources */, DB697DF3278FDDF7004EF2F7 /* TimelineViewModel.swift in Sources */, DB235EF42834DD0900398FCA /* SettingListViewModel.swift in Sources */, DB83020D273D160400BF5224 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */, @@ -2900,6 +2932,7 @@ DB71C7D3271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift in Sources */, DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */, DB36F35E257F74C10028F81E /* ScrollViewContainer.swift in Sources */, + DBEA97662A1B58E200C8B75B /* SecondaryContainerViewModel.swift in Sources */, DB76A65D275F52D500A50673 /* MediaInfoDescriptionView+ViewModel.swift in Sources */, DB66DB902823AC3C0071F5F3 /* TabBarItem.swift in Sources */, DB697E0927904BFB004EF2F7 /* FederatedTimelineViewController.swift in Sources */, @@ -2981,7 +3014,9 @@ DB1D3DED28938ACD008F0BD0 /* HistoryViewModel.swift in Sources */, DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */, DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, + DB0455B62A1CA2F3009A00EF /* NewColumnViewModel.swift in Sources */, DBAA898E2758CF01001C273B /* DrawerSidebarHeaderView+ViewModel.swift in Sources */, + DBEA97682A1B6D2300C8B75B /* NewColumnViewController.swift in Sources */, DBADCDC82826658700D1CA4E /* MediaPreviewableViewController.swift in Sources */, DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */, DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */, @@ -3186,6 +3221,7 @@ DB76A64D275DFA0600A50673 /* TwitterStatusThreadReplyViewModel+State.swift in Sources */, DBF87AD92892A67D0029A7C7 /* TranslateButtonPreferenceView.swift in Sources */, DB76A66A27606F6900A50673 /* MediaPreviewVideoViewController.swift in Sources */, + DB0455B82A1CA510009A00EF /* NewColumnView.swift in Sources */, DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */, DB88E6292890DF67009A01F5 /* BehaviorsPreferenceViewController.swift in Sources */, DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */, diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 8f948a88..c7a4d8cd 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -271,7 +271,7 @@ extension SceneCoordinator { } -private extension SceneCoordinator { +extension SceneCoordinator { func get(scene: Scene) -> UIViewController? { let viewController: UIViewController? diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index 387b5a17..bb90adba 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -96,9 +96,7 @@ extension StatusSection { // ) // } return cell - - case .topLoader: let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell cell.activityIndicatorView.startAnimating() diff --git a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift index 122d8d9c..7503f3ce 100644 --- a/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift +++ b/TwidereX/Scene/List/HomeList/HomeListStatusTimelineViewController.swift @@ -47,6 +47,12 @@ extension HomeListStatusTimelineViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 18c67e5c..8e9d91fe 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -104,6 +104,12 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 8f37139d..bdad4f28 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -37,12 +37,19 @@ final class ContentSplitViewController: UIViewController, NeedsDependency { return mainTabBarController }() + private(set) lazy var secondaryContainerViewController: SecondaryContainerViewController = { + let viewController = SecondaryContainerViewController(context: context, coordinator: coordinator, authContext: authContext) + return viewController + }() + private(set) lazy var secondaryTabBarController: SecondaryTabBarController = { let secondaryTabBarController = SecondaryTabBarController(context: context, coordinator: coordinator, authContext: authContext) return secondaryTabBarController }() var mainTabBarViewLeadingLayoutConstraint: NSLayoutConstraint! + var mainTabBarViewTrailingLayoutConstraint: NSLayoutConstraint! + var mainTabBarViewWidthLayoutConstraint: NSLayoutConstraint! @Published var isSidebarDisplay = false @Published var isSecondaryTabBarControllerActive = false @@ -101,14 +108,27 @@ extension ContentSplitViewController { view.addSubview(mainTabBarController.view) mainTabBarController.didMove(toParent: self) mainTabBarViewLeadingLayoutConstraint = mainTabBarController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor) + mainTabBarViewTrailingLayoutConstraint = mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + mainTabBarViewWidthLayoutConstraint = mainTabBarController.view.widthAnchor.constraint(equalToConstant: 428).priority(.required - 1) NSLayoutConstraint.activate([ mainTabBarController.view.topAnchor.constraint(equalTo: view.topAnchor), mainTabBarViewLeadingLayoutConstraint, mainTabBarController.view.leadingAnchor.constraint(equalTo: sidebarViewController.view.trailingAnchor, constant: UIView.separatorLineHeight(of: view)).priority(.required - 1), - mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainTabBarViewTrailingLayoutConstraint, mainTabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + addChild(secondaryContainerViewController) + secondaryContainerViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(secondaryContainerViewController.view) + secondaryContainerViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + secondaryContainerViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + secondaryContainerViewController.view.leadingAnchor.constraint(equalTo: mainTabBarController.view.trailingAnchor, constant: UIView.separatorLineHeight(of: view)), // 1pt for divider + secondaryContainerViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + secondaryContainerViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + addChild(secondaryTabBarController) secondaryTabBarController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(secondaryTabBarController.view) @@ -163,9 +183,28 @@ extension ContentSplitViewController { case .regular: isSidebarDisplay = true mainTabBarViewLeadingLayoutConstraint.isActive = false + let width: CGFloat = { + var minWidth = UIScreen.main.bounds.width + if UIScreen.main.bounds.height < minWidth { + minWidth = UIScreen.main.bounds.height + } + if let window = view.window, window.frame.width < minWidth { + minWidth = window.frame.width + } + return minWidth - ContentSplitViewController.sidebarWidth + }() + let mainWidth = width / 100 * 55 + let secondaryWidth = width / 100 * 45 + secondaryContainerViewController.viewModel.update(width: floor(secondaryWidth)) + mainTabBarViewTrailingLayoutConstraint.isActive = false + mainTabBarViewWidthLayoutConstraint.constant = floor(mainWidth) + mainTabBarViewWidthLayoutConstraint.isActive = true + default: isSidebarDisplay = false mainTabBarViewLeadingLayoutConstraint.isActive = true + mainTabBarViewWidthLayoutConstraint.isActive = false + mainTabBarViewTrailingLayoutConstraint.isActive = true } guard let previousTraitCollection = previousTraitCollection else { return } diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift new file mode 100644 index 00000000..a2f01e71 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -0,0 +1,40 @@ +// +// NewColumnView.swift +// TwidereX +// +// Created by MainasuK on 2023/5/23. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import SwiftUI + +protocol NewColumnViewDelegate: AnyObject { + func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) +} + +struct NewColumnView: View { + @ObservedObject var viewModel: NewColumnViewModel + + var body: some View { + List { + ForEach(viewModel.tabs, id: \.self) { tab in + Button { + viewModel.delegate?.newColumnView(viewModel, tabBarItemDidPressed: tab) + } label: { + HStack { + Image(uiImage: tab.image) + Text("\(tab.title)") + Spacer() + Image(systemName: "chevron.right") + } // end HStack + .font(.subheadline) + .foregroundColor(Color.primary) + } + .buttonStyle(.borderless) + } // end ForEach + } // end List + .listStyle(.plain) + } // end body +} + diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift new file mode 100644 index 00000000..779d9049 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewController.swift @@ -0,0 +1,55 @@ +// +// NewColumnViewController.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import SwiftUI + +final class NewColumnViewController: UIViewController { + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + let authContext: AuthContext + + public private(set) lazy var viewModel = NewColumnViewModel(context: context, auth: authContext) + private lazy var contentView = NewColumnView(viewModel: viewModel) + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self.coordinator = coordinator + self.authContext = authContext + super.init(nibName: nil, bundle: nil) + // end init + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension NewColumnViewController { + override func viewDidLoad() { + super.viewDidLoad() + + title = "New Column" + + let hostingViewController = UIHostingController(rootView: contentView) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + addChild(hostingViewController) + view.addSubview(hostingViewController.view) + hostingViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } +} diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift new file mode 100644 index 00000000..5af7bd74 --- /dev/null +++ b/TwidereX/Scene/Root/NewColumn/NewColumnViewModel.swift @@ -0,0 +1,73 @@ +// +// NewColumnViewModel.swift +// TwidereX +// +// Created by MainasuK on 2023/5/23. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import Combine + +final class NewColumnViewModel: ObservableObject { + + weak var delegate: NewColumnViewDelegate? + + // input + let context: AppContext + let auth: AuthContext + + @Published var preferredEnableHistory: Bool = false + + // output + var tabs: [TabBarItem] { + switch auth.authenticationContext { + case .twitter: + var results: [TabBarItem] = [ + .home, + .notification, + .search, + .me, + .likes, + ] + if preferredEnableHistory { + results.append(.history) + } + results.append(.lists) + results.append(.trends) + return results + case .mastodon: + var results: [TabBarItem] = [ + .home, + .notification, + .search, + .me, + .local, + .federated, + .likes, + ] + if preferredEnableHistory { + results.append(.history) + } + results.append(.lists) + results.append(.trends) + return results + } + } + + // output + + init( + context: AppContext, + auth: AuthContext + ) { + self.context = context + self.auth = auth + // end init + + UserDefaults.shared.publisher(for: \.preferredEnableHistory).removeDuplicates() + .receive(on: DispatchQueue.main) + .assign(to: &$preferredEnableHistory) + } + +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift new file mode 100644 index 00000000..9aca7a88 --- /dev/null +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift @@ -0,0 +1,224 @@ +// +// SecondaryContainerViewController.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit + +final class SecondaryContainerViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + let authContext: AuthContext + + public private(set) lazy var viewModel = SecondaryContainerViewModel(context: context, auth: authContext) + + let containerScrollView = UIScrollView() + let stack = UIStackView() + + init( + context: AppContext, + coordinator: SceneCoordinator, + authContext: AuthContext + ) { + self.context = context + self.coordinator = coordinator + self.authContext = authContext + super.init(nibName: nil, bundle: nil) + // end init + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SecondaryContainerViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + containerScrollView.frame = view.bounds + containerScrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(containerScrollView) + NSLayoutConstraint.activate([ + containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), + containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), + containerScrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), + containerScrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + stack.axis = .horizontal + stack.spacing = UIView.separatorLineHeight(of: view) + stack.alignment = .leading + + stack.translatesAutoresizingMaskIntoConstraints = false + containerScrollView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.topAnchor), + stack.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor), + stack.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor), + stack.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor), + ]) + + setupNewColumn() + } + +} + +extension SecondaryContainerViewController { + private func setupNewColumn() { + let newColumnViewController = NewColumnViewController( + context: context, + coordinator: coordinator, + authContext: authContext + ) + newColumnViewController.viewModel.delegate = self + viewModel.addColumn(in: stack, viewController: newColumnViewController, setupColumnMenu: false) + } +} + +// MARK: - NewColumnViewDelegate +extension SecondaryContainerViewController: NewColumnViewDelegate { + func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) { + switch tab { + case .home: + let homeTimelineViewController = HomeTimelineViewController() + configure(viewController: homeTimelineViewController) + homeTimelineViewController.viewModel = HomeTimelineViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: homeTimelineViewController + ) + case .homeList: + assertionFailure() + case .notification: + let notificationViewController = NotificationViewController() + configure(viewController: notificationViewController) + notificationViewController.viewModel = NotificationViewModel( + context: context, + authContext: authContext, + coordinator: coordinator + ) + self.viewModel.addColumn( + in: stack, + viewController: notificationViewController + ) + case .search: + let searchViewController = SearchViewController() + configure(viewController: searchViewController) + searchViewController.viewModel = SearchViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: searchViewController + ) + case .me: + let profileViewController = ProfileViewController() + configure(viewController: profileViewController) + profileViewController.viewModel = MeProfileViewModel( + context: context, + authContext: authContext + ) + self.viewModel.addColumn( + in: stack, + viewController: profileViewController + ) + case .local: + let federatedTimelineViewModel = FederatedTimelineViewModel( + context: context, + authContext: authContext, + isLocal: true + ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .federated: + let federatedTimelineViewModel = FederatedTimelineViewModel( + context: context, + authContext: authContext, + isLocal: false + ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .messages: + assertionFailure() + case .likes: + let userLikeTimelineViewModel = UserLikeTimelineViewModel( + context: context, + authContext: authContext, + timelineContext: .init( + timelineKind: .like, + protected: { + guard let user = authContext.authenticationContext.user(in: context.managedObjectContext) else { return false } + return user.protected + }(), + userIdentifier: authContext.authenticationContext.userIdentifier + ) + ) + userLikeTimelineViewModel.isFloatyButtonDisplay = false + guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .history: + let historyViewModel = HistoryViewModel( + context: context, + coordinator: coordinator, + authContext: authContext + ) + guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .lists: + guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } + + let compositeListViewModel = CompositeListViewModel( + context: context, + authContext: authContext, + kind: .lists(me) + ) + guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .trends: + let trendViewModel = TrendViewModel( + context: context, + authContext: authContext + ) + guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return } + self.viewModel.addColumn( + in: stack, + viewController: rootViewController + ) + case .drafts: + assertionFailure() + case .settings: + assertionFailure() + } + } + + private func configure(viewController: NeedsDependency) { + viewController.context = context + viewController.coordinator = coordinator + } +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift new file mode 100644 index 00000000..6964eeee --- /dev/null +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -0,0 +1,146 @@ +// +// SecondaryContainerViewModel.swift +// TwidereX +// +// Created by MainasuK on 2023/5/22. +// Copyright © 2023 Twidere. All rights reserved. +// + +import UIKit +import TwidereCommon + +class SecondaryContainerViewModel: ObservableObject { + + // input + let context: AppContext + let auth: AuthContext + + @Published private(set) var width: CGFloat = 375 + + // output + @Published private var viewControllers: [UINavigationController] = [] + + init( + context: AppContext, + auth: AuthContext + ) { + self.context = context + self.auth = auth + // end init + } + +} + +extension SecondaryContainerViewModel { + func addColumn( + in stack: UIStackView, + viewController: UIViewController, + setupColumnMenu: Bool = true + ) { + let navigationController = UINavigationController(rootViewController: viewController) + viewControllers.append(navigationController) + + let count = stack.arrangedSubviews.count + if count == 0 { + stack.addArrangedSubview(navigationController.view) + } else { + stack.insertArrangedSubview(navigationController.view, at: count - 1) + } + + navigationController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + navigationController.view.widthAnchor.constraint(equalToConstant: width).identifier("width"), + navigationController.view.heightAnchor.constraint(equalTo: stack.heightAnchor), + ]) + + if setupColumnMenu { + setupColumnMenuBarButtonItem( + in: stack, + viewController: viewController, + navigationController: navigationController + ) + } + } + + func update(width: CGFloat) { + for viewController in viewControllers { + guard let constraint = viewController.view.constraints.first(where: { $0.identifier == "width" }) else { + continue + } + constraint.constant = width + } + + self.width = width + } +} + +extension SecondaryContainerViewModel { + private func setupColumnMenuBarButtonItem( + in stack: UIStackView, + viewController: UIViewController, + navigationController: UINavigationController + ) { + let barButtonItem = UIBarButtonItem() + barButtonItem.image = UIImage(systemName: "slider.horizontal.3") + let deferredMenuElement = UIDeferredMenuElement.uncached { [weak self, weak stack, weak viewController, weak navigationController] handler in + guard let self = self, + let stack = stack, + let viewController = viewController, + let navigationController = navigationController + else { + handler([]) + return + } + + var menuElements: [UIMenuElement] = [] + + let closeColumnAction = UIAction(title: "Close column", image: UIImage(systemName: "xmark.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + navigationController.view.removeFromSuperview() + navigationController.view.isHidden = true + self.viewControllers.removeAll(where: { $0 === navigationController }) + } + let closeMenu = UIMenu(title: "", options: .displayInline, children: [closeColumnAction]) + menuElements.append(closeMenu) + + let _index: Int? = stack.arrangedSubviews.firstIndex(where: { view in + return navigationController.view === view + }) + if let index = _index { + var moveMenuElements: [UIMenuElement] = [] + if index > 0 { + let moveLeftMenuAction = UIAction(title: "Move left", image: UIImage(systemName: "arrow.left.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + stack.insertArrangedSubview(navigationController.view, at: index - 1) + } + moveMenuElements.append(moveLeftMenuAction) + } + if index < stack.arrangedSubviews.count - 2 { + let moveRightMenuAction = UIAction(title: "Move Right", image: UIImage(systemName: "arrow.right.square")) { [weak self, weak stack, weak navigationController] _ in + guard let self = self else { return } + guard let stack = stack else { return } + guard let navigationController = navigationController else { return } + stack.removeArrangedSubview(navigationController.view) + stack.insertArrangedSubview(navigationController.view, at: index + 1) + } + moveMenuElements.append(moveRightMenuAction) + } + if !moveMenuElements.isEmpty { + let moveMenu = UIMenu(title: "", options: .displayInline, children: moveMenuElements) + menuElements.append(moveMenu) + } + } + + let menu = UIMenu(title: "", options: .displayInline, children: menuElements) + handler([menu]) + } + barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: [deferredMenuElement]) + viewController.navigationItem.leftBarButtonItem = barButtonItem + } +} diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift index b27a6799..546e56b7 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewController.swift @@ -87,6 +87,12 @@ extension TimelineViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) From 3daa983e726862ea2be7cc5b1a4c078968dbf7f5 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 23 May 2023 20:32:26 +0800 Subject: [PATCH 11/17] chore: update to version 2.0.0 (132) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index a535b3bd..fc2ffac4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 131 + 132 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 74c8e822..f7b5ede6 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 8486256b..a8899289 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 132; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index b8160ed3..6e57623d 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index f312c6c0..e5e4a38b 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 25b47773..04af1923 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 25b47773..04af1923 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 131 + 132 From 2f76328209c5a382d74448f723db4019a533256d Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 22:09:28 +0800 Subject: [PATCH 12/17] feat: add tweet detail and update layout margin for iPad column. Fix the top conversation link not display issue --- TwidereSDK/Package.swift | 6 +- .../CoreDataStack/TwitterStatus.swift | 16 +- .../TwitterSDK/Twitter+Entity+V2+Tweet.swift | 19 ++ .../Vendor/SwiftTwitterTextProvider.swift | 40 --- .../StatusFetchViewModel+Timeline+Home.swift | 63 ++--- .../Content/StatusView+ViewModel.swift | 237 ++++++------------ .../TextViewRepresentable.swift | 107 ++++---- .../xcshareddata/swiftpm/Package.resolved | 12 +- .../NotificationViewController.swift | 6 + .../Scene/Profile/ProfileViewController.swift | 1 - .../StatusThreadViewModel+Diffable.swift | 1 + 11 files changed, 219 insertions(+), 289 deletions(-) create mode 100644 TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift delete mode 100644 TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 007e3ef9..c5656ca6 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -29,7 +29,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.5.2"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", exact: "4.6.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/Alamofire/AlamofireNetworkActivityIndicator.git", from: "3.1.0"), @@ -43,7 +43,6 @@ let package = Package( .package(url: "https://github.com/aheze/Popovers.git", from: "1.3.2"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), - .package(url: "https://github.com/TwidereProject/twitter-text.git", exact: "0.0.3"), .package(url: "https://github.com/MainasuK/DateTools", branch: "master"), .package(url: "https://github.com/kciter/Floaty.git", branch: "master"), .package(url: "https://github.com/MainasuK/FPSIndicator.git", from: "1.1.0"), @@ -51,7 +50,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.6.0"), .package(url: "https://github.com/tid-kijyun/Kanna.git", from: "5.2.7"), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), - .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.8.0"), + .package(url: "https://github.com/TwidereProject/TwitterSDK.git", exact: "0.9.1"), .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), .package(name: "CoverFlowStackLayout", path: "../CoverFlowStackLayout"), ], @@ -106,7 +105,6 @@ let package = Package( .product(name: "AlamofireImage", package: "AlamofireImage"), .product(name: "AlamofireNetworkActivityIndicator", package: "AlamofireNetworkActivityIndicator"), .product(name: "MetaTextKit", package: "MetaTextKit"), - .product(name: "TwitterText", package: "twitter-text"), .product(name: "DateToolsSwift", package: "DateTools"), .product(name: "CryptoSwift", package: "CryptoSwift"), .product(name: "Kanna", package: "Kanna"), diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift index 90197b0a..4728b3a1 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterStatus.swift @@ -28,15 +28,15 @@ extension TwitterStatus { continue } - // drop quote URL - if let quote = quote { - let quoteID = quote.id - guard !displayURL.hasPrefix("twitter.com"), - !expandedURL.hasPrefix(quoteID) - else { - text = text.replacingOccurrences(of: shortURL, with: "") - continue + // drop twitter URL + // - quote URL: remove URL + // - long tweet self URL suffix: replace "… URL" with "…" + if displayURL.hasPrefix("twitter.com") && expandedURL.localizedCaseInsensitiveContains("/status/") { + if expandedURL.localizedCaseInsensitiveContains(self.id) { + text = text.replacingOccurrences(of: "… " + shortURL, with: "…") } + text = text.replacingOccurrences(of: shortURL, with: "") + continue } } return text diff --git a/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift b/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift new file mode 100644 index 00000000..dff3e777 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Extension/TwitterSDK/Twitter+Entity+V2+Tweet.swift @@ -0,0 +1,19 @@ +// +// Twitter+Entity+V2+Tweet.swift +// +// +// Created by MainasuK on 2023/6/2. +// + +import Foundation +import TwitterSDK +import TwitterMeta + +extension Twitter.Entity.V2.Tweet { + public var urlEntities: [TwitterContent.URLEntity] { + let results = entities?.urls?.map { entity in + TwitterContent.URLEntity(url: entity.url, expandedURL: entity.expandedURL, displayURL: entity.displayURL) + } + return results ?? [] + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift b/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift deleted file mode 100644 index 1bb9ab29..00000000 --- a/TwidereSDK/Sources/TwidereCore/Vendor/SwiftTwitterTextProvider.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SwiftTwitterTextProvider.swift -// -// -// Created by MainasuK on 2023/2/3. -// - -import Foundation -import TwitterText -import TwitterMeta - -public class SwiftTwitterTextProvider: TwitterTextProvider { - - public func parse(text: String) -> TwitterMeta.ParseResult { - let result = Parser.defaultParser.parseTweet(text: text) - return .init( - isValid: result.isValid, - weightedLength: result.weightedLength, - maxWeightedLength: Parser.defaultParser.maxWeightedTweetLength(), - entities: self.entities(in: text) - ) - } - - public func entities(in text: String) -> [TwitterMeta.TwitterTextProviderEntity] { - return TwitterText.entities(in: text).compactMap { entity in - switch entity.type { - case .url: return .url(range: entity.range) - case .screenName: return .screenName(range: entity.range) - case .hashtag: return .hashtag(range: entity.range) - case .listname: return .listName(range: entity.range) - case .symbol: return .symbol(range: entity.range) - case .tweetChar: return .tweetChar(range: entity.range) - case .tweetEmojiChar: return .tweetEmojiChar(range: entity.range) - } - } - } - - public init() { } - -} diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift index 5ea4cd75..1682e3c7 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Home.swift @@ -94,35 +94,40 @@ extension StatusFetchViewModel.Timeline.Home { public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): - let responses = try await api.twitterHomeTimeline( - query: .init( - sinceID: fetchContext.sinceID, - untilID: fetchContext.untilID, - paginationToken: nil, - maxResults: fetchContext.maxResults ?? 100, - onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) - ), - authenticationContext: fetchContext.authenticationContext - ) - let nextInput: Input? = { - guard let last = responses.last, - last.value.meta.nextToken != nil, - let oldestID = last.value.meta.oldestID - else { return nil } - let fetchContext = fetchContext.map(untilID: oldestID) - return .twitter(fetchContext) - }() - return .init( - result: { - let statuses = responses - .map { $0.value.data } - .compactMap{ $0 } - .flatMap { $0 } - return .twitterV2(statuses) - }(), - backInput: nil, - nextInput: nextInput.flatMap { .home($0) } - ) + do { + let responses = try await api.twitterHomeTimeline( + query: .init( + sinceID: fetchContext.sinceID, + untilID: fetchContext.untilID, + paginationToken: nil, + maxResults: fetchContext.maxResults ?? 100, + onlyMedia: fetchContext.filter.rule.contains(.onlyMedia) + ), + authenticationContext: fetchContext.authenticationContext + ) + let nextInput: Input? = { + guard let last = responses.last, + last.value.meta.nextToken != nil, + let oldestID = last.value.meta.oldestID + else { return nil } + let fetchContext = fetchContext.map(untilID: oldestID) + return .twitter(fetchContext) + }() + return .init( + result: { + let statuses = responses + .map { $0.value.data } + .compactMap{ $0 } + .flatMap { $0 } + return .twitterV2(statuses) + }(), + backInput: nil, + nextInput: nextInput.flatMap { .home($0) } + ) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(error.localizedDescription)") + throw error + } case .mastodon(let fetchContext): let authenticationContext = fetchContext.authenticationContext let responses = try await api.mastodonHomeTimeline( diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index ab809e0e..0b831841 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -297,6 +297,31 @@ extension StatusView { } } +extension StatusView.ViewModel { + @MainActor + public func updateTwitterStatusContent(statusID: TwitterStatus.ID) async throws { + do { + guard case let .twitter(authenticationContext) = authContext?.authenticationContext else { return } + let response = try await Twitter.API.V2.Status.detail( + session: URLSession(configuration: .ephemeral), + query: .init(statusID: statusID), + authorization: authenticationContext.authorization + ) + let metaContent = TwitterMetaContent.convert( + document: TwitterContent(content: response.value.text, urlEntities: response.value.urlEntities), + urlMaximumLength: .max, + twitterTextProvider: SwiftTwitterTextProvider(), + useParagraphMark: true + ) + self.content = metaContent + // delegate?.statusView(self, translateContentDidChange: status) + } catch { + debugPrint(error.localizedDescription) + throw error + } + } +} + //extension StatusView.ViewModel { // func bind(statusView: StatusView) { // bindHeader(statusView: statusView) @@ -936,14 +961,46 @@ extension StatusView.ViewModel { var containerMargin: CGFloat { var width: CGFloat = 0 + + // container margin switch kind { - case .timeline, .referenceReplyTo: + case .conversationThread: + fallthrough + case .referenceReplyTo: + fallthrough + case .referenceQuote: + fallthrough + case .timeline: width += StatusView.hangingAvatarButtonDimension width += StatusView.hangingAvatarButtonTrailingSpacing - width += 2 * 16 // cell margin - default: + case .repost: + break + case .quote: + break + case .conversationRoot: break } + + // manually readable margin (iPad multi-column layout) + switch kind { + case .timeline: + fallthrough + case .conversationThread: + fallthrough + case .conversationRoot: + if viewLayoutFrame.layoutFrame.width == viewLayoutFrame.readableContentLayoutFrame.width { + width += 2 * 16 + } + case .referenceReplyTo: + break + case .referenceQuote: + break + case .repost: + break + case .quote: + break + } + return width } @@ -1131,14 +1188,25 @@ extension StatusView.ViewModel { } // content - let content = TwitterContent(content: status.displayText, urlEntities: status.urlEntities) - let metaContent = TwitterMetaContent.convert( - document: content, - urlMaximumLength: .max, - twitterTextProvider: SwiftTwitterTextProvider(), - useParagraphMark: true - ) - self.content = metaContent + switch kind { + case .conversationRoot where status.hasMore: + let statusID = status.id + defer { + Task { + try? await self.updateTwitterStatusContent(statusID: statusID) + } + } + fallthrough + default: + let content = TwitterContent(content: status.displayText, urlEntities: status.urlEntities) + let metaContent = TwitterMetaContent.convert( + document: content, + urlMaximumLength: .max, + twitterTextProvider: SwiftTwitterTextProvider(), + useParagraphMark: true + ) + self.content = metaContent + } // language status.publisher(for: \.language) @@ -1426,152 +1494,5 @@ extension StatusView.ViewModel { ) return viewModel -// -// if let repost = status.repost { -// let _repostViewModel = StatusView.ViewModel( -// status: repost, -// authContext: authContext, -// kind: kind, -// delegate: delegate, -// parentViewModel: self, -// viewLayoutFramePublisher: viewLayoutFramePublisher -// ) -// repostViewModel = _repostViewModel -// -// // header - repost -// let _statusHeaderViewModel = StatusHeaderView.ViewModel( -// image: Asset.Media.repeat.image.withRenderingMode(.alwaysTemplate), -// label: { -// let name = status.author.name -// let userRepostText = L10n.Common.Controls.Status.userBoosted(name) -// let text = MastodonContent(content: userRepostText, emojis: status.author.emojis.asDictionary) -// let label = MastodonMetaContent.convert(text: text) -// return label -// }() -// ) -// _statusHeaderViewModel.hasHangingAvatar = _repostViewModel.hasHangingAvatar -// _repostViewModel.statusHeaderViewModel = _statusHeaderViewModel -// } -// -// // author -// status.author.publisher(for: \.avatar) -// .compactMap { $0.flatMap { URL(string: $0) } } -// .assign(to: &$avatarURL) -// status.author.publisher(for: \.displayName) -// .compactMap { _ in status.author.nameMetaContent } -// .assign(to: &$authorName) -// status.author.publisher(for: \.username) -// .map { _ in status.author.acct } -// .assign(to: &$authorUsernme) -// authorUserIdentifier = .mastodon(.init(domain: status.author.domain, id: status.author.id)) -// -// // visibility -// visibility = status.visibility -// -// // timestamp -// switch kind { -// case .conversationRoot: -// break -// default: -// timestampLabelViewModel = TimestampLabelView.ViewModel(timestamp: status.createdAt) -// } -// -// // spoiler content -// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { -// do { -// let content = MastodonContent(content: spoilerText, emojis: status.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) -// self.spoilerContent = metaContent -// } catch { -// assertionFailure(error.localizedDescription) -// self.spoilerContent = nil -// } -// } -// -// // content -// do { -// let content = MastodonContent(content: status.content, emojis: status.emojis.asDictionary) -// let metaContent = try MastodonMetaContent.convert(document: content, useParagraphMark: true) -// self.content = metaContent -// } catch { -// assertionFailure(error.localizedDescription) -// self.content = PlaintextMetaContent(string: "") -// } -// -// // language -// status.publisher(for: \.language) -// .assign(to: &$language) -// -// // content warning -// isContentSensitiveToggled = status.isContentSensitiveToggled -// status.publisher(for: \.isContentSensitiveToggled) -// .receive(on: DispatchQueue.main) -// .assign(to: \.isContentSensitiveToggled, on: self) -// .store(in: &disposeBag) -// -// // media -// mediaViewModels = MediaView.ViewModel.viewModels(from: status) -// -// // poll -// if let poll = status.poll { -// self.pollViewModel = PollView.ViewModel( -// authContext: authContext, -// poll: .mastodon(object: poll) -// ) -// } -// -// // media content warning -// isMediaSensitive = status.isMediaSensitive -// isMediaSensitiveToggled = status.isMediaSensitiveToggled -// status.publisher(for: \.isMediaSensitiveToggled) -// .receive(on: DispatchQueue.main) -// .assign(to: \.isMediaSensitiveToggled, on: self) -// .store(in: &disposeBag) -// -// // toolbar -// toolbarViewModel.platform = .mastodon -// status.publisher(for: \.replyCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$replyCount) -// status.publisher(for: \.repostCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$repostCount) -// status.publisher(for: \.likeCount) -// .map { Int($0) } -// .assign(to: &toolbarViewModel.$likeCount) -// if case let .mastodon(authenticationContext) = authContext?.authenticationContext { -// status.publisher(for: \.likeBy) -// .map { users -> Bool in -// let ids = users.map { $0.id } -// return ids.contains(authenticationContext.userID) -// } -// .assign(to: &toolbarViewModel.$isLiked) -// status.publisher(for: \.repostBy) -// .map { users -> Bool in -// let ids = users.map { $0.id } -// return ids.contains(authenticationContext.userID) -// } -// .assign(to: &toolbarViewModel.$isReposted) -// } else { -// // do nothing -// } -// -// // metric -// switch kind { -// case .conversationRoot: -// let _metricViewModel = StatusMetricView.ViewModel(platform: .mastodon, timestamp: status.createdAt) -// metricViewModel = _metricViewModel -// status.publisher(for: \.replyCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$replyCount) -// status.publisher(for: \.repostCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$repostCount) -// status.publisher(for: \.likeCount) -// .map { Int($0) } -// .assign(to: &_metricViewModel.$likeCount) -// default: -// break -// } } } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index f1886334..c1e09180 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -13,19 +13,8 @@ import MetaTextKit import MetaTextArea public struct TextViewRepresentable: UIViewRepresentable { - - let textView: WrappedTextView = { - let textView = WrappedTextView() - textView.backgroundColor = .clear - textView.isScrollEnabled = false - textView.isEditable = false - textView.isSelectable = false - textView.textContainerInset = .zero - textView.textContainer.lineFragmentPadding = 0 - textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - textView.setContentHuggingPriority(.defaultHigh, for: .vertical) - return textView - }() + // let logger = Logger(subsystem: "TextViewRepresentable", category: "View") + let logger = Logger(.disabled) // input let metaContent: MetaContent @@ -34,6 +23,9 @@ public struct TextViewRepresentable: UIViewRepresentable { let isSelectable: Bool let handler: (Meta) -> Void + // output + let attributedString: NSAttributedString + public init( metaContent: MetaContent, textStyle: TextStyle, @@ -46,53 +38,82 @@ public struct TextViewRepresentable: UIViewRepresentable { self.width = width self.isSelectable = isSelectable self.handler = handler + self.attributedString = { + let attributedString = NSMutableAttributedString(string: metaContent.string) + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: textStyle.textColor, + ] + let linkAttributes: [NSAttributedString.Key: Any] = [ + .font: textStyle.font, + .foregroundColor: UIColor.tintColor, + ] + let paragraphStyle: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.lineSpacing = 3 + style.paragraphSpacing = 8 + return style + }() + + MetaText.setAttributes( + for: attributedString, + textAttributes: textAttributes, + linkAttributes: linkAttributes, + paragraphStyle: paragraphStyle, + content: metaContent + ) + + return attributedString + }() } public func makeUIView(context: Context) -> UITextView { - let textView = self.textView + let textView: WrappedTextView = { + let textView = WrappedTextView() + textView.backgroundColor = .clear + textView.isScrollEnabled = false + textView.isEditable = false + textView.isSelectable = false + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + textView.setContentHuggingPriority(.defaultHigh, for: .vertical) + return textView + }() textView.isSelectable = isSelectable textView.delegate = context.coordinator textView.textViewDelegate = context.coordinator - - let attributedString = NSMutableAttributedString(string: metaContent.string) - let textAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: textStyle.textColor, - ] - let linkAttributes: [NSAttributedString.Key: Any] = [ - .font: textStyle.font, - .foregroundColor: UIColor.tintColor, - ] - let paragraphStyle: NSMutableParagraphStyle = { - let style = NSMutableParagraphStyle() - style.lineSpacing = 3 - style.paragraphSpacing = 8 - return style - }() - - MetaText.setAttributes( - for: attributedString, - textAttributes: textAttributes, - linkAttributes: linkAttributes, - paragraphStyle: paragraphStyle, - content: metaContent - ) - textView.frame.size.width = width textView.textStorage.setAttributedString(attributedString) textView.invalidateIntrinsicContentSize() textView.setNeedsLayout() textView.layoutIfNeeded() - return textView } public func updateUIView(_ view: UITextView, context: Context) { + let textView = view + + var needsLayout = false + defer { + if needsLayout { + textView.invalidateIntrinsicContentSize() + textView.setNeedsLayout() + textView.layoutIfNeeded() + } + } + if textView.frame.size.width != width { textView.frame.size.width = width - textView.invalidateIntrinsicContentSize() - textView.setNeedsLayout() - textView.layoutIfNeeded() + needsLayout = true + } + if textView.attributedText != attributedString { + textView.textStorage.setAttributedString(attributedString) + needsLayout = true + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update textView \(view.hashValue): \(metaContent.string)") + } else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reuse textView content") } } diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 33da201f..9d88ff9e 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "addb2b4875a2a5712e1c50add410c560a9cf42f2", - "version": "4.5.2" + "revision": "8877640bba088661e768fc531b5fc9c5976a6e48", + "version": "4.6.0" } }, { @@ -249,8 +249,8 @@ "repositoryURL": "https://github.com/TwidereProject/twitter-text.git", "state": { "branch": null, - "revision": "c88fa4ed8dd7441f827942b83d0564101bbdc508", - "version": "0.0.3" + "revision": "aaa067a823a61f27780692a836ec78fd0c0706fc", + "version": "0.0.4" } }, { @@ -258,8 +258,8 @@ "repositoryURL": "https://github.com/TwidereProject/TwitterSDK.git", "state": { "branch": null, - "revision": "1b2998a2fc5b7abc421e61b075ba3807ba694991", - "version": "0.8.0" + "revision": "8148362dbf900b6f6416e3d64b3332fc660bd844", + "version": "0.9.1" } }, { diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 8aae0660..45716a4d 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -65,6 +65,12 @@ extension NotificationViewController { .receive(on: DispatchQueue.main) .sink { [weak self] needsSetupAvatarBarButtonItem in guard let self = self else { return } + if let leftBarButtonItem = self.navigationItem.leftBarButtonItem, + leftBarButtonItem !== self.avatarBarButtonItem + { + // allow override + return + } self.navigationItem.leftBarButtonItem = needsSetupAvatarBarButtonItem ? self.avatarBarButtonItem : nil } .store(in: &disposeBag) diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index 8e9d91fe..381c674a 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -124,7 +124,6 @@ extension ProfileViewController { self.avatarBarButtonItem.configure(user: user) } .store(in: &disposeBag) - } addChild(tabBarPagerController) diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index 6c727763..b79a65ba 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -34,6 +34,7 @@ extension StatusThreadViewModel { let viewModel = StatusView.ViewModel( status: status, authContext: self.authContext, + kind: .conversationThread, delegate: cell, viewLayoutFramePublisher: self.$viewLayoutFrame ) From e615958da4acb987e6c1dd2807dea51ad77c61ca Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 22:10:26 +0800 Subject: [PATCH 13/17] chore: update version to 2.0.0 (133) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index fc2ffac4..d24aea0b 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 132 + 133 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index f7b5ede6..557f0357 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index a8899289..48e8a626 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 132; + CURRENT_PROJECT_VERSION = 133; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 6e57623d..a82269fb 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index e5e4a38b..4d893e66 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 04af1923..712d01e3 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 04af1923..712d01e3 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 132 + 133 From 3cc0d14e214f7d0f69b177f5ea1ccdf60febd6d5 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:02:20 +0800 Subject: [PATCH 14/17] feat: add open tab menu actions --- .../Scene/Root/NewColumn/NewColumnView.swift | 18 +++ .../SecondaryContainerViewController.swift | 117 ++++++++++-------- .../SecondaryContainerViewModel.swift | 51 +++++++- 3 files changed, 130 insertions(+), 56 deletions(-) diff --git a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift index a2f01e71..a6504df9 100644 --- a/TwidereX/Scene/Root/NewColumn/NewColumnView.swift +++ b/TwidereX/Scene/Root/NewColumn/NewColumnView.swift @@ -11,6 +11,7 @@ import SwiftUI protocol NewColumnViewDelegate: AnyObject { func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) + func newColumnView(_ viewModel: NewColumnViewModel, source: UINavigationController, openTabMenuAction tab: TabBarItem) } struct NewColumnView: View { @@ -38,3 +39,20 @@ struct NewColumnView: View { } // end body } +extension NewColumnView { + static func menu( + tabs: [TabBarItem], + viewModel: NewColumnViewModel, + source: UINavigationController + ) -> UIMenu { + let actions: [UIAction] = tabs.map { tab in + UIAction(title: tab.title, image: tab.image) { [weak viewModel, weak source] _ in + guard let source = source, + let viewModel = viewModel + else { return } + viewModel.delegate?.newColumnView(viewModel, source: source, openTabMenuAction: tab) + } + } + return UIMenu(options: .displayInline, children: actions) + } +} diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift index 9aca7a88..d4f1adb9 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewController.swift @@ -78,13 +78,43 @@ extension SecondaryContainerViewController { authContext: authContext ) newColumnViewController.viewModel.delegate = self - viewModel.addColumn(in: stack, viewController: newColumnViewController, setupColumnMenu: false) + viewModel.addColumn( + in: stack, + tab: nil, + viewController: newColumnViewController, + setupColumnMenu: false + ) } } // MARK: - NewColumnViewDelegate extension SecondaryContainerViewController: NewColumnViewDelegate { func newColumnView(_ viewModel: NewColumnViewModel, tabBarItemDidPressed tab: TabBarItem) { + guard let viewController = self.viewController(for: tab) else { + assertionFailure() + return + } + + self.viewModel.addColumn( + in: stack, + tab: tab, + viewController: viewController, + newColumnViewModel: viewModel + ) + } + + private func menuActionForOpenTabs(tabs: [TabBarItem], exclude: TabBarItem) -> [TabBarItem] { + var tabs = tabs + tabs.removeAll(where: { $0 == exclude }) + return tabs + } + + private func configure(viewController: NeedsDependency) { + viewController.context = context + viewController.coordinator = coordinator + } + + private func viewController(for tab: TabBarItem) -> UIViewController? { switch tab { case .home: let homeTimelineViewController = HomeTimelineViewController() @@ -93,12 +123,10 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: homeTimelineViewController - ) + return homeTimelineViewController case .homeList: assertionFailure() + return nil case .notification: let notificationViewController = NotificationViewController() configure(viewController: notificationViewController) @@ -107,10 +135,7 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { authContext: authContext, coordinator: coordinator ) - self.viewModel.addColumn( - in: stack, - viewController: notificationViewController - ) + return notificationViewController case .search: let searchViewController = SearchViewController() configure(viewController: searchViewController) @@ -118,10 +143,7 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: searchViewController - ) + return searchViewController case .me: let profileViewController = ProfileViewController() configure(viewController: profileViewController) @@ -129,34 +151,26 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { context: context, authContext: authContext ) - self.viewModel.addColumn( - in: stack, - viewController: profileViewController - ) + return profileViewController case .local: let federatedTimelineViewModel = FederatedTimelineViewModel( context: context, authContext: authContext, isLocal: true ) - guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return nil } + return rootViewController case .federated: let federatedTimelineViewModel = FederatedTimelineViewModel( context: context, authContext: authContext, isLocal: false ) - guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .federatedTimeline(viewModel: federatedTimelineViewModel)) else { return nil } + return rootViewController case .messages: assertionFailure() + return nil case .likes: let userLikeTimelineViewModel = UserLikeTimelineViewModel( context: context, @@ -171,54 +185,55 @@ extension SecondaryContainerViewController: NewColumnViewDelegate { ) ) userLikeTimelineViewModel.isFloatyButtonDisplay = false - guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .userLikeTimeline(viewModel: userLikeTimelineViewModel)) else { return nil } + return rootViewController case .history: let historyViewModel = HistoryViewModel( context: context, coordinator: coordinator, authContext: authContext ) - guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .history(viewModel: historyViewModel)) else { return nil } + return rootViewController case .lists: - guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return } + guard let me = authContext.authenticationContext.user(in: context.managedObjectContext)?.asRecord else { return nil } let compositeListViewModel = CompositeListViewModel( context: context, authContext: authContext, kind: .lists(me) ) - guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .compositeList(viewModel: compositeListViewModel)) else { return nil } + return rootViewController case .trends: let trendViewModel = TrendViewModel( context: context, authContext: authContext ) - guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return } - self.viewModel.addColumn( - in: stack, - viewController: rootViewController - ) + guard let rootViewController = coordinator.get(scene: .trend(viewModel: trendViewModel)) else { return nil } + return rootViewController case .drafts: assertionFailure() + return nil case .settings: assertionFailure() + return nil } } - private func configure(viewController: NeedsDependency) { - viewController.context = context - viewController.coordinator = coordinator + func newColumnView( + _ viewModel: NewColumnViewModel, + source: UINavigationController, + openTabMenuAction tab: TabBarItem + ) { + guard let index = self.viewModel.removeColumn(in: stack, navigationController: source) else { return } + guard let viewController = self.viewController(for: tab) else { return } + self.viewModel.addColumn( + in: stack, + at: index, + tab: tab, + viewController: viewController, + newColumnViewModel: viewModel + ) } } diff --git a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift index 6964eeee..df4b05c7 100644 --- a/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift +++ b/TwidereX/Scene/Root/SecondaryContainer/SecondaryContainerViewModel.swift @@ -34,8 +34,11 @@ class SecondaryContainerViewModel: ObservableObject { extension SecondaryContainerViewModel { func addColumn( in stack: UIStackView, + at index: Int? = nil, + tab: TabBarItem?, viewController: UIViewController, - setupColumnMenu: Bool = true + setupColumnMenu: Bool = true, + newColumnViewModel: NewColumnViewModel? = nil ) { let navigationController = UINavigationController(rootViewController: viewController) viewControllers.append(navigationController) @@ -44,7 +47,8 @@ extension SecondaryContainerViewModel { if count == 0 { stack.addArrangedSubview(navigationController.view) } else { - stack.insertArrangedSubview(navigationController.view, at: count - 1) + let at = min(count - 1, index ?? count - 1) + stack.insertArrangedSubview(navigationController.view, at: at) } navigationController.view.translatesAutoresizingMaskIntoConstraints = false @@ -56,8 +60,10 @@ extension SecondaryContainerViewModel { if setupColumnMenu { setupColumnMenuBarButtonItem( in: stack, + tab: tab, viewController: viewController, - navigationController: navigationController + navigationController: navigationController, + newColumnViewModel: newColumnViewModel ) } } @@ -72,13 +78,32 @@ extension SecondaryContainerViewModel { self.width = width } + + func removeColumn( + in stack: UIStackView, + navigationController: UINavigationController + ) -> Int? { + let _index: Int? = stack.arrangedSubviews.firstIndex(where: { view in + navigationController.view === view + }) + guard let index = _index else { return nil } + + stack.removeArrangedSubview(navigationController.view) + navigationController.view.removeFromSuperview() + navigationController.view.isHidden = true + self.viewControllers.removeAll(where: { $0 === navigationController }) + + return index + } } extension SecondaryContainerViewModel { private func setupColumnMenuBarButtonItem( in stack: UIStackView, + tab: TabBarItem?, viewController: UIViewController, - navigationController: UINavigationController + navigationController: UINavigationController, + newColumnViewModel: NewColumnViewModel? = nil ) { let barButtonItem = UIBarButtonItem() barButtonItem.image = UIImage(systemName: "slider.horizontal.3") @@ -140,7 +165,23 @@ extension SecondaryContainerViewModel { let menu = UIMenu(title: "", options: .displayInline, children: menuElements) handler([menu]) } - barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: [deferredMenuElement]) + + var children: [UIMenuElement] = [deferredMenuElement] + + if let newColumnViewModel = newColumnViewModel, let tab = tab { + var tabs = newColumnViewModel.tabs + tabs.removeAll(where: { $0 == tab }) + if !tabs.isEmpty { + let openTabsMenu: UIMenu = NewColumnView.menu( + tabs: tabs, + viewModel: newColumnViewModel, + source: navigationController + ) + children.append(openTabsMenu) + } + } + + barButtonItem.menu = UIMenu(title: "", options: .displayInline, children: children) viewController.navigationItem.leftBarButtonItem = barButtonItem } } From 046f7e85a3fc457697304f51a3c8405d28ca8a32 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:02:58 +0800 Subject: [PATCH 15/17] chore: update version to 2.0.0 (134) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index d24aea0b..92b7b1fc 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 133 + 134 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 557f0357..6cc92700 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 48e8a626..18ebe929 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 133; + CURRENT_PROJECT_VERSION = 134; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index a82269fb..3f104d0b 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 4d893e66..6553c720 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 712d01e3..c2b59739 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 712d01e3..c2b59739 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 133 + 134 From 90c4ce8b7ff5bbd6d42db2dbf12efff93e3a05f2 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:15:40 +0800 Subject: [PATCH 16/17] fix: layout loop issue --- .../UIViewRepresentable/TextViewRepresentable.swift | 2 +- .../Sources/TwidereUI/Utility/ViewLayoutFrame.swift | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift index c1e09180..a600fbd7 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/TextViewRepresentable.swift @@ -107,7 +107,7 @@ public struct TextViewRepresentable: UIViewRepresentable { textView.frame.size.width = width needsLayout = true } - if textView.attributedText != attributedString { + if textView.attributedText.string != attributedString.string { textView.textStorage.setAttributedString(attributedString) needsLayout = true diff --git a/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift index 817651eb..db92e782 100644 --- a/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift +++ b/TwidereSDK/Sources/TwidereUI/Utility/ViewLayoutFrame.swift @@ -37,21 +37,19 @@ extension ViewLayoutFrame { let layoutFrame = view.frame if self.layoutFrame != layoutFrame { self.layoutFrame = layoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") } let safeAreaLayoutFrame = view.safeAreaLayoutGuide.layoutFrame if self.safeAreaLayoutFrame != safeAreaLayoutFrame { self.safeAreaLayoutFrame = safeAreaLayoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") } let readableContentLayoutFrame = view.readableContentGuide.layoutFrame if self.readableContentLayoutFrame != readableContentLayoutFrame { self.readableContentLayoutFrame = readableContentLayoutFrame + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") } - - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): layoutFrame: \(layoutFrame.debugDescription)") - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): safeAreaLayoutFrame: \(safeAreaLayoutFrame.debugDescription)") - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): readableContentLayoutFrame: \(readableContentLayoutFrame.debugDescription)") - } } From 3f56a9f53a675c43fdf152c4a3d187229a736f2c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 2 Jun 2023 23:16:10 +0800 Subject: [PATCH 17/17] chore: update version to 2.0.0 (135) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 36 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 92b7b1fc..ea7e4eed 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 134 + 135 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 6cc92700..16f087d4 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 18ebe929..094934e4 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3467,7 +3467,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3494,7 +3494,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3517,7 +3517,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3541,7 +3541,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3568,7 +3568,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -3597,7 +3597,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3626,7 +3626,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3654,7 +3654,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; @@ -3680,7 +3680,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3707,7 +3707,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3861,7 +3861,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3893,7 +3893,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3920,7 +3920,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3945,7 +3945,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3968,7 +3968,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -3991,7 +3991,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; @@ -4015,7 +4015,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; @@ -4042,7 +4042,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 3f104d0b..8149c3ff 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 6553c720..06462e58 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index c2b59739..256cbafa 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index c2b59739..256cbafa 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 2.0.0 CFBundleVersion - 134 + 135