From e58dac04f09995e2c57d1102dd7f6f025fb68687 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 7 Jul 2022 18:15:53 +0800 Subject: [PATCH 01/38] feat: add Mastodon push notification subscriber --- AppShared/AppSecret.swift | 9 +- Gemfile | 6 + Gemfile.lock | 109 ++++++++ Podfile | 5 +- Podfile.lock | 14 +- README.md | 4 +- TwidereSDK/Package.swift | 2 + .../MastodonSDK/API/Mastodon+API+Push.swift | 237 ++++++++++++++++++ .../Sources/TwidereCommon/AppSecret.swift | 82 +++++- .../APIService/APIService+Notification.swift | 63 +++++ .../Service/AuthenticationService.swift | 8 +- ...cationService+NotificationSubscriber.swift | 100 ++++++++ .../Service/NotificationService.swift | 155 ++++++++++++ .../TwidereCore/State/AppContext.swift | 14 +- .../xcshareddata/swiftpm/Package.resolved | 9 + .../List/AccountListViewModel+Diffable.swift | 2 +- TwidereX/Supporting Files/AppDelegate.swift | 27 ++ TwidereX/TwidereX.entitlements | 2 + 18 files changed, 832 insertions(+), 16 deletions(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift create mode 100644 TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift create mode 100644 TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift create mode 100644 TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift diff --git a/AppShared/AppSecret.swift b/AppShared/AppSecret.swift index 9bafaf4b..cf94c505 100644 --- a/AppShared/AppSecret.swift +++ b/AppShared/AppSecret.swift @@ -16,7 +16,14 @@ extension AppSecret { let keys = TwidereXKeys() self.init( secret: keys.app_secret, - oauthSecret: oauthSecret + oauthSecret: oauthSecret, + mastodonNotificationRelayEndpoint: { + #if DEBUG + return keys.mastodon_notification_endpoint_debug + #else + return keys.mastodon_notification_endpoint + #endif + }() ) } diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..48aae3d8 --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "cocoapods" +gem "cocoapods-clean" +gem "cocoapods-keys" + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..d7d0aaf8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,109 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.5) + rexml + RubyInline (3.12.6) + ZenTest (~> 4.3) + ZenTest (4.12.1) + activesupport (6.1.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + claide (1.1.0) + cocoapods (1.11.3) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.11.3) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.4.0, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 1.0, < 3.0) + xcodeproj (>= 1.21.0, < 2.0) + cocoapods-clean (0.0.1) + cocoapods-core (1.11.3) + activesupport (>= 5.0, < 7) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (1.6.3) + cocoapods-keys (2.2.1) + dotenv + osx_keychain + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored2 (3.1.2) + concurrent-ruby (1.1.10) + dotenv (2.7.6) + escape (0.0.4) + ethon (0.15.0) + ffi (>= 1.15.0) + ffi (1.15.5) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.8.3) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + json (2.6.2) + minitest (5.16.2) + molinillo (0.8.0) + nanaimo (0.3.0) + nap (1.1.0) + netrc (0.11.0) + osx_keychain (1.0.2) + RubyInline (~> 3) + public_suffix (4.0.7) + rexml (3.2.5) + ruby-macho (2.5.1) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + xcodeproj (1.22.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + zeitwerk (2.6.0) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + cocoapods + cocoapods-clean + cocoapods-keys + +BUNDLED WITH + 2.2.32 diff --git a/Podfile b/Podfile index 9160b39a..7f3df865 100644 --- a/Podfile +++ b/Podfile @@ -22,6 +22,7 @@ target 'TwidereX' do pod 'Firebase/AnalyticsWithoutAdIdSupport' pod 'FirebaseCrashlytics' pod 'FirebasePerformance' + pod 'FirebaseMessaging' # misc pod 'SwiftGen', '~> 6.3.0' @@ -60,7 +61,9 @@ plugin 'cocoapods-keys', { "oauth_endpoint", "oauth_endpoint_debug", "oauth2_endpoint", - "oauth2_endpoint_debug" + "oauth2_endpoint_debug", + "mastodon_notification_endpoint_debug", + "mastodon_notification_endpoint" ] } diff --git a/Podfile.lock b/Podfile.lock index 9c6267ee..78f31965 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -40,6 +40,15 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/UserDefaults (~> 7.7) - PromisesObjC (~> 2.1) + - FirebaseMessaging (9.2.0): + - FirebaseCore (~> 9.0) + - FirebaseInstallations (~> 9.0) + - GoogleDataTransport (< 10.0.0, >= 9.1.4) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Reachability (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) - FirebasePerformance (9.1.0): - FirebaseCore (~> 9.0) - FirebaseInstallations (~> 9.0) @@ -105,6 +114,7 @@ DEPENDENCIES: - DateToolsSwift (~> 5.0.0) - Firebase/AnalyticsWithoutAdIdSupport - FirebaseCrashlytics + - FirebaseMessaging - FirebasePerformance - FLEX (~> 4.4.0) - Keys (from `Pods/CocoaPodsKeys`) @@ -125,6 +135,7 @@ SPEC REPOS: - FirebaseCoreInternal - FirebaseCrashlytics - FirebaseInstallations + - FirebaseMessaging - FirebasePerformance - FirebaseRemoteConfig - FLEX @@ -153,6 +164,7 @@ SPEC CHECKSUMS: FirebaseCoreInternal: f3c838ae0b41cd840bbf34f90debfede78d352e5 FirebaseCrashlytics: 8e21736dcf15d814b79229eb7e79ba3a5eec5f30 FirebaseInstallations: a91c74276b544524ce144c8315d8cdbce2dbe30b + FirebaseMessaging: 4eaf1b8a7464b2c5e619ad66e9b20ee3e3206b24 FirebasePerformance: c31b7f18df1fda622edd4190d2d6fcececec3e3c FirebaseRemoteConfig: ebd0eb74b9a0593ab2a3f0788a5be5595b46a05c FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab @@ -168,6 +180,6 @@ SPEC CHECKSUMS: XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: b8b276d991a162e817f90fbd698f266887207b71 +PODFILE CHECKSUM: 5e12e332eb31dd3e2519e9f9f18f66b6116e66e1 COCOAPODS: 1.11.3 diff --git a/README.md b/README.md index cee63894..32a419d8 100644 --- a/README.md +++ b/README.md @@ -51,18 +51,20 @@ The localization resource files locate in [TwidereX-Localization](https://github ## Acknowledgements +- [Alamofire](https://github.com/Alamofire/Alamofire) - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) -- [Alamofire](https://github.com/Alamofire/Alamofire) - [cocoapods-keys](https://github.com/orta/cocoapods-keys) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) - [DateToolSwift](https://github.com/MatthewYork/DateTools) - [Floaty](https://github.com/kciter/Floaty) - [Kanna](https://github.com/tid-kijyun/Kanna) +- [KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess) - [Kingfisher](https://github.com/onevcat/Kingfisher) - [LineChart](https://github.com/nhatminh12369/LineChart) - [PageBoy](https://github.com/uias/Pageboy) +- [Popovers](https://github.com/aheze/Popovers) - [swift-nio](https://github.com/apple/swift-nio) - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftMessages](https://github.com/SwiftKickMobile/SwiftMessages) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 126a913b..63ed187f 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -42,6 +42,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.2"), .package(url: "https://github.com/SwiftKickMobile/SwiftMessages.git", from: "9.0.5"), .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(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), ], targets: [ @@ -86,6 +87,7 @@ let package = Package( name: "TwidereCommon", dependencies: [ "TwitterSDK", + .product(name: "KeychainAccess", package: "KeychainAccess"), ] ), .target( diff --git a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift new file mode 100644 index 00000000..eb69f012 --- /dev/null +++ b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift @@ -0,0 +1,237 @@ +// +// Mastodon+API+Push.swift +// +// +// Created by MainasuK on 2022-7-7. +// + +import Foundation + +extension Mastodon.API { + public enum Push { } +} + +extension Mastodon.API.Push { + + private static func subscriptionEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("push") + .appendingPathComponent("subscription") + } + + /// Get current subscription + /// + /// Using this endpoint to get current subscription + /// + /// - Since: 2.4.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/4/25 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/notifications/push/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: User token. Could be nil if status is public + /// - Returns: `Subscription` nested in the response + public static func subscription( + session: URLSession, + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let request = Mastodon.API.request( + url: subscriptionEndpointURL(domain: domain), + method: .GET, + query: nil, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + +} + +extension Mastodon.API.Push { + + /// Subscribe to push notifications + /// + /// Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted. + /// + /// - Since: 2.4.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/4/25 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/notifications/push/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: User token. Could be nil if status is public + /// - Returns: `Subscription` nested in the response + public static func createSubscription( + session: URLSession, + domain: String, + query: CreateSubscriptionQuery, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let request = Mastodon.API.request( + url: subscriptionEndpointURL(domain: domain), + method: .POST, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + + public struct CreateSubscriptionQuery: JSONEncodeQuery { + let subscription: QuerySubscription + let data: QueryData + + public init( + subscription: Mastodon.API.Push.QuerySubscription, + data: Mastodon.API.Push.QueryData + ) { + self.subscription = subscription + self.data = data + } + + var queryItems: [URLQueryItem]? { nil } + } + +} + +extension Mastodon.API.Push { + + /// Change types of notifications + /// + /// Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead. + /// + /// - Since: 2.4.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/4/25 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/notifications/push/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: User token. Could be nil if status is public + /// - Returns: `Subscription` nested in the response + public static func updateSubscription( + session: URLSession, + domain: String, + query: UpdateSubscriptionQuery, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let request = Mastodon.API.request( + url: subscriptionEndpointURL(domain: domain), + method: .PUT, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Mastodon.API.decode(type: Mastodon.Entity.Subscription.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + + public struct UpdateSubscriptionQuery: JSONEncodeQuery { + + let data: QueryData + + public init(data: Mastodon.API.Push.QueryData) { + self.data = data + } + + var queryItems: [URLQueryItem]? { nil } + } + +} + +extension Mastodon.API.Push { + + /// Remove current subscription + /// + /// Removes the current Web Push API subscription. + /// + /// - Since: 2.4.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/4/26 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/notifications/push/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: User token. Could be nil if status is public + /// - Returns: `Subscription` nested in the response + public static func removeSubscription( + session: URLSession, + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let request = Mastodon.API.request( + url: subscriptionEndpointURL(domain: domain), + method: .DELETE, + query: nil, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Mastodon.API.decode(type: Mastodon.Entity.EmptySubscription.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + +} + +extension Mastodon.API.Push { + + public struct QuerySubscription: Codable { + let endpoint: String + let keys: Keys + + public init( + endpoint: String, + keys: Keys + ) { + self.endpoint = endpoint + self.keys = keys + } + + public struct Keys: Codable { + let p256dh: String + let auth: String + + public init(p256dh: Data, auth: Data) { + self.p256dh = p256dh.base64UrlEncodedString() + self.auth = auth.base64UrlEncodedString() + } + } + } + + public struct QueryData: Codable { + let alerts: Alerts + + public init(alerts: Mastodon.API.Push.QueryData.Alerts) { + self.alerts = alerts + } + + public struct Alerts: Codable { + let favourite: Bool? + let follow: Bool? + let reblog: Bool? + let mention: Bool? + let poll: Bool? + + public init(favourite: Bool?, follow: Bool?, reblog: Bool?, mention: Bool?, poll: Bool?) { + self.favourite = favourite + self.follow = follow + self.reblog = reblog + self.mention = mention + self.poll = poll + } + } + } + +} diff --git a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift index f9ef9b18..45ed0987 100644 --- a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift +++ b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift @@ -8,20 +8,47 @@ import Foundation import CryptoKit import TwitterSDK +import KeychainAccess public class AppSecret { - + public typealias Secret = String + // keychain + public static let keychain = Keychain(service: "com.twidere.TwidereX.keychain", accessGroup: AppCommon.groupID) + + // notification key names + static let mastodonNotificationPrivateKeyName = "notification-private-key-base64" + static let mastodonNotificationAuthName = "notification-auth-base64" + public let secret: Secret public let oauthSecret: OAuthSecret + + // Mastodon notification keys + public let mastodonNotificationRelayEndpoint: String + public var mastodonNotificationPrivateKey: P256.KeyAgreement.PrivateKey { + AppSecret.createOrFetchNotificationPrivateKey() + } + public var mastodonNotificationPublicKey: P256.KeyAgreement.PublicKey { + mastodonNotificationPrivateKey.publicKey + } + public var mastodonNotificationAuth: Data { + AppSecret.createOrFetchNotificationAuth() + } public init( secret: Secret, - oauthSecret: AppSecret.OAuthSecret + oauthSecret: AppSecret.OAuthSecret, + mastodonNotificationRelayEndpoint: String ) { self.secret = secret self.oauthSecret = oauthSecret + self.mastodonNotificationRelayEndpoint = mastodonNotificationRelayEndpoint + } + + public static func register() { + _ = AppSecret.createOrFetchNotificationPrivateKey() + _ = AppSecret.createOrFetchNotificationAuth() } } @@ -107,3 +134,54 @@ extension AppSecret { return wrapKey } } + +extension AppSecret { + + private static func createOrFetchNotificationPrivateKey() -> P256.KeyAgreement.PrivateKey { + if let encoded = AppSecret.keychain[AppSecret.mastodonNotificationPrivateKeyName], + let data = Data(base64Encoded: encoded) { + do { + let privateKey = try P256.KeyAgreement.PrivateKey(rawRepresentation: data) + return privateKey + } catch { + assertionFailure() + return AppSecret.resetNotificationPrivateKey() + } + } else { + return AppSecret.resetNotificationPrivateKey() + } + } + + private static func resetNotificationPrivateKey() -> P256.KeyAgreement.PrivateKey { + let privateKey = P256.KeyAgreement.PrivateKey() + keychain[AppSecret.mastodonNotificationPrivateKeyName] = privateKey.rawRepresentation.base64EncodedString() + return privateKey + } + +} + +extension AppSecret { + + private static func createOrFetchNotificationAuth() -> Data { + if let encoded = keychain[AppSecret.mastodonNotificationAuthName], + let data = Data(base64Encoded: encoded) { + return data + } else { + return AppSecret.resetNotificationAuth() + } + } + + private static func resetNotificationAuth() -> Data { + let auth = AppSecret.createRandomAuthBytes() + keychain[AppSecret.mastodonNotificationAuthName] = auth.base64EncodedString() + return auth + } + + private static func createRandomAuthBytes() -> Data { + let byteCount = 16 + var bytes = Data(count: byteCount) + _ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) } + return bytes + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift new file mode 100644 index 00000000..021288e4 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift @@ -0,0 +1,63 @@ +// +// APIService+Notification.swift +// +// +// Created by MainasuK on 2022-7-7. +// + +import os.log +import Foundation +import MastodonSDK +import TwidereCommon + +extension APIService { + + func createMastodonNotificationSubscription( + query: Mastodon.API.Push.CreateSubscriptionQuery, + authenticationContext: MastodonAuthenticationContext + ) async throws { + + let response = try await Mastodon.API.Push.createSubscription( + session: session, + domain: authenticationContext.domain, + query: query, + authorization: authenticationContext.authorization + ) + +// .flatMap { response -> AnyPublisher, Error> in +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: create subscription successful %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.endpoint) +// +// let managedObjectContext = self.backgroundManagedObjectContext +// return managedObjectContext.performChanges { +// guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else { +// assertionFailure() +// return +// } +// subscription.endpoint = response.value.endpoint +// subscription.serverKey = response.value.serverKey +// subscription.userToken = authorization.accessToken +// subscription.didUpdate(at: response.networkDate) +// } +// .setFailureType(to: Error.self) +// .map { _ in return response } +// .eraseToAnyPublisher() +// } +// .eraseToAnyPublisher() + } + + func cancelMastodonNotificationSubscription( + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) async throws -> Mastodon.Response.Content { + let response = try await Mastodon.API.Push.removeSubscription( + session: session, + domain: domain, + authorization: authorization + ) + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel subscription successful: \(domain), \(String(describing: authorization))") + + return response + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift index 971479e5..c0a15737 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift @@ -26,7 +26,7 @@ public class AuthenticationService: NSObject { let authenticationIndexFetchedResultsController: NSFetchedResultsController // output - public let authenticationIndexes = CurrentValueSubject<[AuthenticationIndex], Never>([]) + @Published public var authenticationIndexes: [AuthenticationIndex] = [] public let activeAuthenticationIndex = CurrentValueSubject(nil) @Published public var activeAuthenticationContext: AuthenticationContext? = nil @@ -93,7 +93,7 @@ public class AuthenticationService: NSObject { // .store(in: &disposeBag) // bind activeAuthenticationIndex - authenticationIndexes + $authenticationIndexes .map { $0.sorted(by: { $0.activeAt > $1.activeAt }).first } .assign(to: \.value, on: activeAuthenticationIndex) .store(in: &disposeBag) @@ -109,7 +109,7 @@ public class AuthenticationService: NSObject { do { try authenticationIndexFetchedResultsController.performFetch() - authenticationIndexes.value = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] } catch { assertionFailure(error.localizedDescription) } @@ -194,7 +194,7 @@ extension AuthenticationService: NSFetchedResultsControllerDelegate { public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { switch controller { case authenticationIndexFetchedResultsController: - authenticationIndexes.value = authenticationIndexFetchedResultsController.fetchedObjects ?? [] + authenticationIndexes = authenticationIndexFetchedResultsController.fetchedObjects ?? [] default: assertionFailure() } diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift new file mode 100644 index 00000000..a038cf8b --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift @@ -0,0 +1,100 @@ +// +// NotificationService+NotificationViewModel.swift +// +// +// Created by MainasuK on 2022-7-6. +// + +import os.log +import Foundation +import Combine +import CoreDataStack +import MastodonSDK +import TwidereCommon + +public struct NotificationSubject { + public let fcmToken: String? + public let appSecret: AppSecret +} + +public protocol NotificationSubscriber { + var userIdentifier: UserIdentifier { get } + func update(api: APIService, subject: NotificationSubject) +} + +extension NotificationService { + final public class MastodonNotificationSubscriber: NotificationSubscriber { + + let logger = Logger(subsystem: "MastodonNotificationSubscriber", category: "Subscriber") + + var disposeBag = Set() + + // input + public let userIdentifier: UserIdentifier + public let authenticationContext: MastodonAuthenticationContext + + // output + + init(authenticationContext: MastodonAuthenticationContext) { + self.userIdentifier = .mastodon(.init(domain: authenticationContext.domain, id: authenticationContext.userID)) + self.authenticationContext = authenticationContext + // end init + } + + + public func update(api: APIService, subject: NotificationSubject) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update subscription for \(String(describing: self.userIdentifier))") + + Task { + do { + try await self.subscribe(api: api, subject: subject) + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): subscribe notification success") + + } catch { + guard subject.fcmToken != nil else { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): subscribe notification: wait device token…") + return + } + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): subscribe notification fail: \(error.localizedDescription)") + } + } // end + } + } +} + +extension NotificationService.MastodonNotificationSubscriber { + private func subscribe( + api: APIService, + subject: NotificationSubject + ) async throws { + guard let token = subject.fcmToken else { + throw AppError.implicit(.badRequest) + } + + let appSecret = subject.appSecret + let endpoint = appSecret.mastodonNotificationRelayEndpoint + "/" + token + let p256dh = appSecret.mastodonNotificationPublicKey.x963Representation + let auth = appSecret.mastodonNotificationAuth + + try await api.createMastodonNotificationSubscription( + query: .init( + subscription: .init( + endpoint: endpoint, + keys: .init( + p256dh: p256dh, + auth: auth + ) + ), + data: .init(alerts: .init( + favourite: true, + follow: true, + reblog: true, + mention: true, + poll: true + )) + ), + authenticationContext: authenticationContext + ) + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift new file mode 100644 index 00000000..698ef0e7 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -0,0 +1,155 @@ +// +// NotificationService.swift +// +// +// Created by MainasuK on 2022-7-6. +// + +import os.log +import UIKit +import Combine +import CoreDataStack +import TwidereCommon + +final public actor NotificationService { + + let logger = Logger(subsystem: "NotificationService", category: "NotificationService") + + var disposeBag = Set() + + // input + weak var apiService: APIService? + weak var authenticationService: AuthenticationService? + let appSecret: AppSecret + + var isNotificationPermissionGranted = false + var fcmToken: String? { + didSet { + notifySubscribers() + } + } + + // output + var subscribers: [NotificationSubscriber] = [] { + didSet { + notifySubscribers() + } + } +// let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) +// let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) +// let requestRevealNotificationPublisher = PassthroughSubject() + + init( + apiService: APIService, + authenticationService: AuthenticationService, + appSecret: AppSecret + ) { + self.apiService = apiService + self.authenticationService = authenticationService + self.appSecret = appSecret + + authenticationService.$authenticationIndexes + .sink { [weak self] authenticationIndexes in + guard let self = self else { return } + + // request permission when sign-in account + Task { + if !authenticationIndexes.isEmpty { + await self.requestNotificationPermission() + } + } // end Task + + Task { + let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in + AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + } + await self.updateSubscribers(authenticationContexts) + } // end Task + } + .store(in: &disposeBag) // FIXME: how to use disposeBag in actor under Swift 6 ?? + +// Publishers.CombineLatest( +// authenticationService.mastodonAuthentications, +// applicationIconBadgeNeedsUpdate +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] mastodonAuthentications, _ in +// guard let self = self else { return } +// +// var count = 0 +// for authentication in mastodonAuthentications { +// count += UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) +// } +// +// UserDefaults.shared.notificationBadgeCount = count +// UIApplication.shared.applicationIconBadgeNumber = count +// +// self.unreadNotificationCountDidUpdate.send() +// } +// .store(in: &disposeBag) + } + +} + +extension NotificationService { + public func updateToken(_ token: String?) { + fcmToken = token + } +} + +extension NotificationService { + private func notifySubscribers() { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + guard let api = self.apiService else { + assertionFailure() + return + } + + let subject = NotificationSubject( + fcmToken: fcmToken, + appSecret: appSecret + ) + + for subscriber in subscribers { + subscriber.update(api: api, subject: subject) + } + } +} + +extension NotificationService { + private func requestNotificationPermission() async { + do { + let center = UNUserNotificationCenter.current() + let isGranted = try await center.requestAuthorization(options: [.alert, .sound, .badge]) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): request push notification permission: isGranted -> \(isGranted)") + + isNotificationPermissionGranted = isGranted + + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): request push notification permission fail: \(error.localizedDescription)") + } + } + + private func updateSubscribers(_ authenticationContexts: [AuthenticationContext]) async { + subscribers = authenticationContexts.compactMap { + return dequeueSubscriber(authenticationContext: $0) + } + } // end func + + private func dequeueSubscriber( + authenticationContext: AuthenticationContext + ) -> NotificationSubscriber? { + let userIdentifier = authenticationContext.userIdentifier + if let subscriber = subscribers.first(where: { $0.userIdentifier == userIdentifier }) { + return subscriber + } else { + switch authenticationContext { + case .twitter: + return nil + case .mastodon(let authenticationContext): + return MastodonNotificationSubscriber(authenticationContext: authenticationContext) + } // end switch + } + } // end func +} diff --git a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift index e2b649df..704aff74 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift @@ -32,10 +32,7 @@ public class AppContext: ObservableObject { public let photoLibraryService = PhotoLibraryService() public let playerService = PlayerService() - public let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .share() - .eraseToAnyPublisher() + public let notificationService: NotificationService public init(appSecret: AppSecret) { let _coreDataStack = CoreDataStack() @@ -51,18 +48,25 @@ public class AppContext: ObservableObject { ) apiService = _apiService - authenticationService = AuthenticationService( + let _authenticationService = AuthenticationService( managedObjectContext: _managedObjectContext, backgroundManagedObjectContext: _backgroundManagedObjectContext, apiService: _apiService, appSecret: appSecret ) + authenticationService = _authenticationService mastodonEmojiService = MastodonEmojiService() publisherService = PublisherService( apiService: _apiService, appSecret: appSecret ) + + notificationService = NotificationService( + apiService: _apiService, + authenticationService: _authenticationService, + appSecret: appSecret + ) } private func setupStoreReview() { diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index 79407425..2287d087 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -100,6 +100,15 @@ "version": null } }, + { + "package": "KeychainAccess", + "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state": { + "branch": null, + "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", + "version": "4.2.2" + } + }, { "package": "Kingfisher", "repositoryURL": "https://github.com/onevcat/Kingfisher.git", diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index 45bf0471..ca9f8157 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -33,7 +33,7 @@ extension AccountListViewModel { snapshot.appendSections([.main]) diffableDataSource?.apply(snapshot) - context.authenticationService.authenticationIndexes + context.authenticationService.$authenticationIndexes .receive(on: DispatchQueue.main) .sink { [weak self] authenticationIndexes in guard let self = self else { return } diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index 5d809374..48dc7af0 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -5,11 +5,13 @@ // Created by Cirno MainasuK on 2020-8-31. // +import os.log import AVKit import UIKit import Combine import Floaty import Firebase +import FirebaseMessaging import Kingfisher import AppShared import TwidereCommon @@ -18,6 +20,8 @@ import TwidereCommon @main class AppDelegate: UIResponder, UIApplicationDelegate { + + let logger = Logger(subsystem: "AppDelegate", category: "AppDelegate") var disposeBag = Set() @@ -25,9 +29,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + AppSecret.register() + + // setup push notification + UNUserNotificationCenter.current().delegate = self + application.registerForRemoteNotifications() + // Firebase FirebaseApp.configure() Crashlytics.crashlytics().setCustomValue(Locale.preferredLanguages.first ?? "nil", forKey: "preferredLanguage") + Messaging.messaging().delegate = self // configure AudioSession try? AVAudioSession.sharedInstance().setCategory(.ambient) @@ -83,6 +94,22 @@ extension AppDelegate { } } +// MARK: - UNUserNotificationCenterDelegate +extension AppDelegate: UNUserNotificationCenterDelegate { + +} + +// MARK: - MessagingDelegate +extension AppDelegate: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fcmToken: \(fcmToken ?? "")") + + Task { + await appContext.notificationService.updateToken(fcmToken) + } // end Task + } +} + extension AppContext { static var shared: AppContext { let appDelegate = UIApplication.shared.delegate as! AppDelegate diff --git a/TwidereX/TwidereX.entitlements b/TwidereX/TwidereX.entitlements index 7df4ce92..09208841 100644 --- a/TwidereX/TwidereX.entitlements +++ b/TwidereX/TwidereX.entitlements @@ -2,6 +2,8 @@ + aps-environment + development com.apple.developer.siri com.apple.security.application-groups From 77985d8b1cb6d6bb1eb51494f0df1da3fe841a30 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 7 Jul 2022 18:48:24 +0800 Subject: [PATCH 02/38] chore: update README and GitHub Action scripts to use ruby bundle --- .github/scripts/setup.sh | 27 +++++++++++++++------------ README.md | 7 ++++++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index 6e1ccb37..e0413c1e 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -1,17 +1,20 @@ #!/bin/bash -sudo gem install cocoapods-keys +gem install bundle +bundle install # set "TwidereX" project name to make cocoapods-keys using the right project -pod keys set app_secret ${APP_SECRET} "TwidereX" -pod keys set consumer_key ${CONSUMER_KEY} "TwidereX" -pod keys set consumer_key_secret ${CONSUMER_KEY_SECRET} "TwidereX" -pod keys set client_id "" "TwidereX" -pod keys set client_id_debug "" "TwidereX" -pod keys set host_key_public ${HOST_KEY_PUBLIC} "TwidereX" -pod keys set oauth_endpoint ${OAUTH_ENDPOINT} "TwidereX" -pod keys set oauth_endpoint_debug "oob" "TwidereX" -pod keys set oauth2_endpoint ${OAUTH2_ENDPOINT} "TwidereX" -pod keys set oauth2_endpoint_debug ${OAUTH2_ENDPOINT_DEBUG} "TwidereX" +bundle exec pod keys set app_secret ${APP_SECRET} "TwidereX" +bundle exec pod keys set consumer_key ${CONSUMER_KEY} "TwidereX" +bundle exec pod keys set consumer_key_secret ${CONSUMER_KEY_SECRET} "TwidereX" +bundle exec pod keys set client_id "" "TwidereX" +bundle exec pod keys set client_id_debug "" "TwidereX" +bundle exec pod keys set host_key_public ${HOST_KEY_PUBLIC} "TwidereX" +bundle exec pod keys set oauth_endpoint ${OAUTH_ENDPOINT} "TwidereX" +bundle exec pod keys set oauth_endpoint_debug "oob" "TwidereX" +bundle exec pod keys set oauth2_endpoint ${OAUTH2_ENDPOINT} "TwidereX" +bundle exec pod keys set oauth2_endpoint_debug ${OAUTH2_ENDPOINT_DEBUG} "TwidereX" +bundle exec pod keys set mastodon_notification_endpoint_debug "" "TwidereX" +bundle exec pod keys set mastodon_notification_endpoint "" "TwidereX" -pod install \ No newline at end of file +bundle exec pod install \ No newline at end of file diff --git a/README.md b/README.md index 32a419d8..b37aab8f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,11 @@ All you need ```zsh git clone https://github.com/TwidereProject/TwidereX-iOS cd TwidereX-iOS -pod install + +gem install bundle +bundle install + +bundle exec pod install # setup cocoapods-keys > app_secret: "Twidere" @@ -73,6 +77,7 @@ The localization resource files locate in [TwidereX-Localization](https://github - [TOCropViewController](https://github.com/TimOliver/TOCropViewController) - [twitter-text](https://github.com/twitter/twitter-text) - [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile) +- [webpush-fcm-relay](https://github.com/mastodon/webpush-fcm-relay) - [ZIPFoundation](https://github.com/weichsel/ZIPFoundation) ## License From d30f227ecef885f12cff689d483e3a32a8c7ed13 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 7 Jul 2022 19:47:16 +0800 Subject: [PATCH 03/38] feat: decrypt e2e notification content --- NotificationService/Info.plist | 13 ++ .../NotificationService+Decrypt.swift | 81 +++++++ .../NotificationService.entitlements | 10 + NotificationService/NotificationService.swift | 116 ++++++++++ NotificationService/String+Decode85.swift | 53 +++++ NotificationService/String+Escape.swift | 22 ++ .../MastodonPushNotification.swift | 50 ++++ TwidereX.xcodeproj/project.pbxproj | 215 +++++++++++++++++- 8 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 NotificationService/Info.plist create mode 100644 NotificationService/NotificationService+Decrypt.swift create mode 100644 NotificationService/NotificationService.entitlements create mode 100644 NotificationService/NotificationService.swift create mode 100644 NotificationService/String+Decode85.swift create mode 100644 NotificationService/String+Escape.swift create mode 100644 TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist new file mode 100644 index 00000000..57421ebf --- /dev/null +++ b/NotificationService/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/NotificationService/NotificationService+Decrypt.swift b/NotificationService/NotificationService+Decrypt.swift new file mode 100644 index 00000000..34ecda4b --- /dev/null +++ b/NotificationService/NotificationService+Decrypt.swift @@ -0,0 +1,81 @@ +// +// NotificationService+Decrypt.swift +// NotificationService +// +// Created by MainasuK on 2022-7-7. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import Foundation +import CryptoKit + +extension NotificationService { + + static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? { + + guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): failed to craete shared secret") + return nil + } + + let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32) + + let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) + let key = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16) + + let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) + let nonce = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12) + + let nonceData = nonce.withUnsafeBytes(Array.init) + + guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): failed to create sealedBox") + return nil + } + + var _plaintext: Data? + do { + _plaintext = try AES.GCM.open(sealedBox, using: key) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): sealedBox open fail: \(error.localizedDescription)") + } + guard let plaintext = _plaintext else { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): failed to open sealedBox") + return nil + } + + let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1]) + guard plaintext.count >= 2 + paddingLength else { + fatalError() + } + let unpadded = plaintext.suffix(from: paddingLength + 2) + + return Data(unpadded) + } + + static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data { + var info = Data() + + info.append("Content-Encoding: ".data(using: .utf8)!) + info.append(type.data(using: .utf8)!) + info.append(0) + info.append("P-256".data(using: .utf8)!) + info.append(0) + info.append(0) + info.append(65) + info.append(clientPublicKey) + info.append(0) + info.append(65) + info.append(serverPublicKey) + + return info + } +} + +extension NotificationService { + static func publicKey(encodedPublicKey: String) -> P256.KeyAgreement.PublicKey? { + let publicKeyData = encodedPublicKey.decode85() + return try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) + } +} diff --git a/NotificationService/NotificationService.entitlements b/NotificationService/NotificationService.entitlements new file mode 100644 index 00000000..5e2aafe8 --- /dev/null +++ b/NotificationService/NotificationService.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.twidere.twiderex + + + diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift new file mode 100644 index 00000000..be5e9c96 --- /dev/null +++ b/NotificationService/NotificationService.swift @@ -0,0 +1,116 @@ +// +// NotificationService.swift +// NotificationService +// +// Created by MainasuK on 2022-7-7. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UserNotifications +import AppShared +import TwidereCommon +import TwidereCore +import AlamofireImage + +class NotificationService: UNNotificationServiceExtension { + + static let logger = Logger(subsystem: "NotificationService", category: "Service") + var logger: Logger { NotificationService.logger } + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + // Modify the notification content here... + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + // Payload + let privateKey = AppSecret.default.mastodonNotificationPrivateKey + let auth = AppSecret.default.mastodonNotificationAuth + guard let encodedPayload = bestAttemptContent.userInfo["p"] as? String else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid payload", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + let payload = encodedPayload.decode85() + + // publicKey + guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, + let publicKey = NotificationService.publicKey(encodedPublicKey: encodedPublicKey) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid public key", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + + // salt + guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid salt", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + let salt = encodedSalt.decode85() + + // notification + guard let plaintextData = NotificationService.decrypt(payload: payload, salt: salt, auth: auth, privateKey: privateKey, publicKey: publicKey), + let notification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintextData) else { + contentHandler(bestAttemptContent) + return + } + + bestAttemptContent.title = notification.title + bestAttemptContent.subtitle = "" + bestAttemptContent.body = notification.body.escape() + bestAttemptContent.userInfo["plaintext"] = plaintextData + +// let accessToken = notification.accessToken +// UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) +// +// UserDefaults.shared.notificationBadgeCount += 1 +// bestAttemptContent.badge = NSNumber(integerLiteral: UserDefaults.shared.notificationBadgeCount) +// + if let urlString = notification.icon, let url = URL(string: urlString) { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments") + try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) + let filename = UUID().uuidString + ".png" + let fileURL = temporaryDirectoryURL.appendingPathComponent(filename) + + ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in + guard let _ = self else { return } + switch response.result { + case .failure(let error): + NotificationService.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): download image \(url.debugDescription) fail: \(error.localizedDescription)") + case .success(let image): + NotificationService.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): download image \(url.debugDescription) success") + do { + try image.pngData()?.write(to: fileURL) + } catch { + NotificationService.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): image save fail: \(error.localizedDescription)") + } + if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) { + bestAttemptContent.attachments = [attachment] + NotificationService.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): add image attachment success: \(attachment.debugDescription)") + } + } + contentHandler(bestAttemptContent) + }) + } else { + contentHandler(bestAttemptContent) + } + + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + +} diff --git a/NotificationService/String+Decode85.swift b/NotificationService/String+Decode85.swift new file mode 100644 index 00000000..5716dfef --- /dev/null +++ b/NotificationService/String+Decode85.swift @@ -0,0 +1,53 @@ +// +// String+Decode85.swift +// NotificationService +// +// Created by MainasuK on 2022-7-7. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation + +private let DecodeTable: [UInt32] = [ + 0xff, 0x44, 0xff, 0x54, 0x53, 0x52, 0x48, 0xff, + 0x4b, 0x4c, 0x46, 0x41, 0xff, 0x3f, 0x3e, 0x45, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x40, 0xff, 0x49, 0x42, 0x4a, 0x47, + 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, + 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, + 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x3b, 0x3c, 0x3d, 0x4d, 0xff, 0x4e, 0x43, 0xff, + 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x4f, 0xff, 0x50, 0xff, 0xff +] + +extension String { + func decode85() -> Data { + var data = Data() + var block: UInt32 = 0 + var n = 0 + for c in utf8 { + if c >= 32, c < 128, DecodeTable[Int(c - 32)] != 0xff { + let value = DecodeTable[Int(c - 32)] + block = block * 85 + value + n += 1 + if n == 5 { + data.append(UInt8(block >> 24)) + data.append(UInt8((block >> 16) & 0xff)) + data.append(UInt8((block >> 8) & 0xff)) + data.append(UInt8(block & 0xff)) + block = 0 + n = 0 + } + } + } + + if n >= 4 { data.append(UInt8((block >> 16) & 0xff)) } + if n >= 3 { data.append(UInt8((block >> 8) & 0xff)) } + if n >= 2 { data.append(UInt8(block & 0xff)) } + + return data + } +} diff --git a/NotificationService/String+Escape.swift b/NotificationService/String+Escape.swift new file mode 100644 index 00000000..f2e325c5 --- /dev/null +++ b/NotificationService/String+Escape.swift @@ -0,0 +1,22 @@ +// +// String+Escape.swift +// NotificationService +// +// Created by MainasuK on 2022-7-7. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation + +extension String { + func escape() -> String { + return self + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: """, with: "\"") + .replacingOccurrences(of: "'", with: "'") + .replacingOccurrences(of: "'", with: "’") + + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift new file mode 100644 index 00000000..17f69284 --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift @@ -0,0 +1,50 @@ +// +// MastodonPushNotification.swift +// +// +// Created by MainasuK on 2022-7-7. +// + +import Foundation + +public struct MastodonPushNotification: Codable { + + public let accessToken: String + + public let notificationID: Int + public let notificationType: String + + public let preferredLocale: String? + public let icon: String? + public let title: String + public let body: String + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case notificationID = "notification_id" + case notificationType = "notification_type" + case preferredLocale = "preferred_locale" + case icon + case title + case body + } + + public init( + accessToken: String, + notificationID: Int, + notificationType: String, + preferredLocale: String?, + icon: String?, + title: String, + body: String + ) { + self.accessToken = accessToken + self.notificationID = notificationID + self.notificationType = notificationType + self.preferredLocale = preferredLocale + self.icon = icon + self.title = title + self.body = body + } + +} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 93288fa1..4778588a 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -356,6 +356,13 @@ DBFDCE4827F450FC00BE99E3 /* TwidereXIntent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DBFDCE5027F4515E00BE99E3 /* SwitchAccountIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFDCE4F27F4515E00BE99E3 /* SwitchAccountIntentHandler.swift */; }; DBFDCE5427F451FC00BE99E3 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; + DBFE6ECB2876EB42003ABDCB /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFE6ECA2876EB42003ABDCB /* NotificationService.swift */; }; + DBFE6ECF2876EB42003ABDCB /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBFE6EC82876EB42003ABDCB /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DBFE6ED52876F3B4003ABDCB /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; + DBFE6ED62876F3B4003ABDCB /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DBFE6EDB2876F47B003ABDCB /* String+Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFE6EDA2876F47B003ABDCB /* String+Decode85.swift */; }; + DBFE6EDD2876F4D2003ABDCB /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFE6EDC2876F4D2003ABDCB /* NotificationService+Decrypt.swift */; }; + DBFE6EDF2876F567003ABDCB /* String+Escape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFE6EDE2876F567003ABDCB /* String+Escape.swift */; }; E5E65DC85CA480AE17571EFC /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84835D7E14A262E389DD4AB3 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ @@ -423,6 +430,20 @@ remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; remoteInfo = AppShared; }; + DBFE6ECD2876EB42003ABDCB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; + proxyType = 1; + remoteGlobalIDString = DBFE6EC72876EB42003ABDCB; + remoteInfo = NotificationService; + }; + DBFE6ED72876F3B4003ABDCB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; + remoteInfo = AppShared; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -454,11 +475,23 @@ dstSubfolderSpec = 13; files = ( DB77FF7F2847808C00182A0B /* ShareExtension.appex in Embed App Extensions */, + DBFE6ECF2876EB42003ABDCB /* NotificationService.appex in Embed App Extensions */, DBFDCE4827F450FC00BE99E3 /* TwidereXIntent.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; + DBFE6ED92876F3B4003ABDCB /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DBFE6ED62876F3B4003ABDCB /* AppShared.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -871,6 +904,13 @@ DBFDCE4527F450FC00BE99E3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DBFDCE4F27F4515E00BE99E3 /* SwitchAccountIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchAccountIntentHandler.swift; sourceTree = ""; }; DBFDCE5127F451A200BE99E3 /* TwidereXIntent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TwidereXIntent.entitlements; sourceTree = ""; }; + DBFE6EC82876EB42003ABDCB /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DBFE6ECA2876EB42003ABDCB /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + DBFE6ECC2876EB42003ABDCB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DBFE6ED42876EB4A003ABDCB /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + DBFE6EDA2876F47B003ABDCB /* String+Decode85.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Decode85.swift"; sourceTree = ""; }; + DBFE6EDC2876F4D2003ABDCB /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; + DBFE6EDE2876F567003ABDCB /* String+Escape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Escape.swift"; sourceTree = ""; }; F58BC0DB264850C274FDE699 /* Pods-TwidereX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TwidereX.debug.xcconfig"; path = "Target Support Files/Pods-TwidereX/Pods-TwidereX.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -933,6 +973,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBFE6EC52876EB42003ABDCB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DBFE6ED52876F3B4003ABDCB /* AppShared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1990,6 +2038,7 @@ DB94B6BC26C65CB000A2E8A1 /* AppShared */, DBBBBE5F2744E8CC007ACB4B /* ShareExtension */, DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */, + DBFE6EC92876EB42003ABDCB /* NotificationService */, DBDA8E1F24FCF8A3006750DC /* Products */, 0ACC0E3D669CFFCB246A2D5B /* Pods */, F30A8D6C372DFF6B5AEEE327 /* Frameworks */, @@ -2005,6 +2054,7 @@ DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */, DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */, DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */, + DBFE6EC82876EB42003ABDCB /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -2320,6 +2370,19 @@ path = TwidereXIntent; sourceTree = ""; }; + DBFE6EC92876EB42003ABDCB /* NotificationService */ = { + isa = PBXGroup; + children = ( + DBFE6ED42876EB4A003ABDCB /* NotificationService.entitlements */, + DBFE6ECA2876EB42003ABDCB /* NotificationService.swift */, + DBFE6EDC2876F4D2003ABDCB /* NotificationService+Decrypt.swift */, + DBFE6EDA2876F47B003ABDCB /* String+Decode85.swift */, + DBFE6EDE2876F567003ABDCB /* String+Escape.swift */, + DBFE6ECC2876EB42003ABDCB /* Info.plist */, + ); + path = NotificationService; + sourceTree = ""; + }; F30A8D6C372DFF6B5AEEE327 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -2417,6 +2480,7 @@ DB94B6C026C65CB000A2E8A1 /* PBXTargetDependency */, DBFDCE4727F450FC00BE99E3 /* PBXTargetDependency */, DB77FF812847808C00182A0B /* PBXTargetDependency */, + DBFE6ECE2876EB42003ABDCB /* PBXTargetDependency */, ); name = TwidereX; packageProductDependencies = ( @@ -2489,13 +2553,32 @@ productReference = DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */; productType = "com.apple.product-type.app-extension"; }; + DBFE6EC72876EB42003ABDCB /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = DBFE6ED32876EB43003ABDCB /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + DBFE6EC42876EB42003ABDCB /* Sources */, + DBFE6EC52876EB42003ABDCB /* Frameworks */, + DBFE6EC62876EB42003ABDCB /* Resources */, + DBFE6ED92876F3B4003ABDCB /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DBFE6ED82876F3B4003ABDCB /* PBXTargetDependency */, + ); + name = NotificationService; + productName = NotificationService; + productReference = DBFE6EC82876EB42003ABDCB /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ DBDA8E1624FCF8A3006750DC /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1200; ORGANIZATIONNAME = Twidere; TargetAttributes = { @@ -2520,6 +2603,9 @@ DBFDCE3F27F450FC00BE99E3 = { CreatedOnToolsVersion = 13.3; }; + DBFE6EC72876EB42003ABDCB = { + CreatedOnToolsVersion = 14.0; + }; }; }; buildConfigurationList = DBDA8E1924FCF8A3006750DC /* Build configuration list for PBXProject "TwidereX" */; @@ -2559,6 +2645,7 @@ DB94B6BA26C65CB000A2E8A1 /* AppShared */, DBBBBE5D2744E8CC007ACB4B /* ShareExtension */, DBFDCE3F27F450FC00BE99E3 /* TwidereXIntent */, + DBFE6EC72876EB42003ABDCB /* NotificationService */, ); }; /* End PBXProject section */ @@ -2617,6 +2704,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBFE6EC62876EB42003ABDCB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -3174,6 +3268,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBFE6EC42876EB42003ABDCB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DBFE6ECB2876EB42003ABDCB /* NotificationService.swift in Sources */, + DBFE6EDF2876F567003ABDCB /* String+Escape.swift in Sources */, + DBFE6EDD2876F4D2003ABDCB /* NotificationService+Decrypt.swift in Sources */, + DBFE6EDB2876F47B003ABDCB /* String+Decode85.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -3222,6 +3327,16 @@ target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; targetProxy = DBFDCE5627F451FC00BE99E3 /* PBXContainerItemProxy */; }; + DBFE6ECE2876EB42003ABDCB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DBFE6EC72876EB42003ABDCB /* NotificationService */; + targetProxy = DBFE6ECD2876EB42003ABDCB /* PBXContainerItemProxy */; + }; + DBFE6ED82876F3B4003ABDCB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; + targetProxy = DBFE6ED72876F3B4003ABDCB /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -3986,6 +4101,94 @@ }; name = Release; }; + DBFE6ED02876EB43003ABDCB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DBFE6ED12876EB43003ABDCB /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; + DBFE6ED22876EB43003ABDCB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NotificationService; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -4059,6 +4262,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DBFE6ED32876EB43003ABDCB /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DBFE6ED02876EB43003ABDCB /* Debug */, + DBFE6ED12876EB43003ABDCB /* Profile */, + DBFE6ED22876EB43003ABDCB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ From 2ec046eebf8c714c9b7460ed0fd96368162777f1 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 16:56:42 +0800 Subject: [PATCH 04/38] chore: update version to 1.4.2 (110) --- .version | 2 + NotificationService/Info.plist | 4 ++ ShareExtension/Info.plist | 4 +- TwidereX.xcodeproj/project.pbxproj | 75 ++++++++++++------------------ TwidereX/Info.plist | 4 +- TwidereXIntent/Info.plist | 4 ++ TwidereXTests/Info.plist | 4 +- TwidereXUITests/Info.plist | 4 +- ci_scripts/ci_post_clone.sh | 20 ++++++++ release.sh | 4 ++ 10 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 .version create mode 100644 ci_scripts/ci_post_clone.sh create mode 100755 release.sh diff --git a/.version b/.version new file mode 100644 index 00000000..65c1c727 --- /dev/null +++ b/.version @@ -0,0 +1,2 @@ +1.4.2 +110 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 57421ebf..3f322a90 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -2,6 +2,10 @@ + CFBundleVersion + 110 + CFBundleShortVersionString + 1.4.2 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 67828134..96408ea1 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 108 + 110 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 4778588a..28b11a2b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3496,7 +3496,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3504,7 +3504,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3520,7 +3519,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3528,7 +3527,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3543,7 +3541,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3551,7 +3549,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3569,11 +3566,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 108; + DYLIB_CURRENT_VERSION = 110; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3583,7 +3580,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -3601,7 +3597,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3611,7 +3607,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3628,7 +3623,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3640,13 +3635,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3658,11 +3653,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 108; + DYLIB_CURRENT_VERSION = 110; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3672,7 +3667,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -3692,11 +3686,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 108; + DYLIB_CURRENT_VERSION = 110; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3706,7 +3700,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.AppShared; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -3723,7 +3716,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3733,7 +3726,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3750,7 +3742,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3760,7 +3752,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3904,7 +3895,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3912,7 +3903,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3933,7 +3923,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3941,7 +3931,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3957,7 +3946,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3965,7 +3954,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3981,7 +3969,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3989,7 +3977,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4004,7 +3991,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4012,7 +3999,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4027,7 +4013,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4035,7 +4021,6 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -4051,7 +4036,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4063,13 +4048,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -4079,7 +4064,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 108; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4091,13 +4076,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.TwidereXIntent; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -4108,7 +4093,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 110; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -4121,13 +4106,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -4138,7 +4123,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -4150,13 +4135,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -4167,7 +4152,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -4179,13 +4164,13 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 85fb0ee9..20526939 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 108 + 110 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 06915b48..8fea4460 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -2,6 +2,10 @@ + CFBundleVersion + 110 + CFBundleShortVersionString + 1.4.2 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 0487032a..4c2a5f92 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 108 + 110 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 0487032a..4c2a5f92 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 108 + 110 diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100644 index 00000000..dd9ee4a0 --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +gem install bundle +bundle install + +# set "TwidereX" project name to make cocoapods-keys using the right project +bundle exec pod keys set app_secret ${APP_SECRET} "TwidereX" +bundle exec pod keys set consumer_key ${CONSUMER_KEY} "TwidereX" +bundle exec pod keys set consumer_key_secret ${CONSUMER_KEY_SECRET} "TwidereX" +bundle exec pod keys set client_id ${CLIENT_ID} "TwidereX" +bundle exec pod keys set client_id_debug ${CLIENT_ID_DEBUG} "TwidereX" +bundle exec pod keys set host_key_public ${HOST_KEY_PUBLIC} "TwidereX" +bundle exec pod keys set oauth_endpoint ${OAUTH_ENDPOINT} "TwidereX" +bundle exec pod keys set oauth_endpoint_debug "oob" "TwidereX" +bundle exec pod keys set oauth2_endpoint ${OAUTH2_ENDPOINT} "TwidereX" +bundle exec pod keys set oauth2_endpoint_debug ${OAUTH2_ENDPOINT_DEBUG} "TwidereX" +bundle exec pod keys set mastodon_notification_endpoint_debug ${MASTODON_NOTIFICATION_ENDPOINT_DEBUG} "TwidereX" +bundle exec pod keys set mastodon_notification_endpoint ${MASTODON_NOTIFICATION_ENDPOINT} "TwidereX" + +bundle exec pod install diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..96a2e0b7 --- /dev/null +++ b/release.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +agvtool what-marketing-version -terse1 > .version +agvtool what-version -terse >> .version From 4461baea57e5d70355b5b2373f38a9027a7118be Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 17:04:26 +0800 Subject: [PATCH 05/38] chore: use rbenv to workaround Xcode Cloud permission issue --- .version | 2 +- ci_scripts/ci_post_clone.sh | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.version b/.version index 65c1c727..7159ceda 100644 --- a/.version +++ b/.version @@ -1,2 +1,2 @@ 1.4.2 -110 +110 \ No newline at end of file diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index dd9ee4a0..224d51f5 100644 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -1,5 +1,20 @@ #!/bin/sh +# Xcode Cloud scripts + +set -xeu +set -o pipefail + +brew install rbenv +which ruby +echo 'eval "$(rbenv init -)"' >> ~/.zprofile +source ~/.zprofile +which ruby + +rbenv install 3.0.3 +rbenv global 3.0.3 +ruby --version + gem install bundle bundle install From d0d503daef53bf8854b6ef07bb3bef9531b7d411 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 17:14:11 +0800 Subject: [PATCH 06/38] chore: add hardware info list --- .version | 2 +- ci_scripts/ci_post_clone.sh | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.version b/.version index 7159ceda..f802f48e 100644 --- a/.version +++ b/.version @@ -1,2 +1,2 @@ 1.4.2 -110 \ No newline at end of file +110 \ No newline at end of file diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 224d51f5..83a5ebad 100644 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -1,10 +1,14 @@ -#!/bin/sh +#!/bin/zsh # Xcode Cloud scripts set -xeu set -o pipefail +# list hardware +system_profiler SPSoftwareDataType SPHardwareDataType + +# install rbenv brew install rbenv which ruby echo 'eval "$(rbenv init -)"' >> ~/.zprofile @@ -15,7 +19,10 @@ rbenv install 3.0.3 rbenv global 3.0.3 ruby --version +# install bundle gem gem install bundle + +# setup cocoapods bundle install # set "TwidereX" project name to make cocoapods-keys using the right project From 710a69992dbac0d65ca41e0852ce90969db47193 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 19:30:14 +0800 Subject: [PATCH 07/38] chore: migrate to Arkana secrets manager --- .arkana.yml | 27 ++++++++ .github/scripts/setup.sh | 15 +---- AppShared/AppSecret.swift | 65 ------------------- Gemfile | 2 +- Gemfile.lock | 7 ++ Podfile | 18 ----- Podfile.lock | 9 +-- README.md | 19 +++--- TwidereSDK/Package.swift | 4 +- .../Sources/TwidereCommon/AppSecret.swift | 50 ++++++++++++++ TwidereX.xcodeproj/project.pbxproj | 4 -- ci_scripts/ci_post_clone.sh | 17 +---- env/.env | 35 ++++++++++ 13 files changed, 135 insertions(+), 137 deletions(-) create mode 100644 .arkana.yml delete mode 100644 AppShared/AppSecret.swift create mode 100644 env/.env diff --git a/.arkana.yml b/.arkana.yml new file mode 100644 index 00000000..47808557 --- /dev/null +++ b/.arkana.yml @@ -0,0 +1,27 @@ +import_name: 'ArkanaKeys' +namespace: 'Keys' +result_path: 'dependencies' +flavors: + - AppStore +swift_declaration_strategy: let +should_generate_unit_tests: true +package_manager: spm +environments: + - Debug + - Release +global_secrets: + # AppStore build Secret + - AppSecret + - HostKeyPublic +environment_secrets: + # Will lookup for Debug and Release env vars (assuming no flavor was declared) + # Twitter OAuth 1.0a Keys + - ConsumerKey + - ConsumerKeySecret + - OauthEndpoint + # Twitter OAuth 2.0 Keys + - ClientID + - ClientSecret + - Oauth2Endpoint + # Mastodon Push Notification Endpoint + - MastodonNotificationEndpoint diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index e0413c1e..2915c343 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -3,18 +3,5 @@ gem install bundle bundle install -# set "TwidereX" project name to make cocoapods-keys using the right project -bundle exec pod keys set app_secret ${APP_SECRET} "TwidereX" -bundle exec pod keys set consumer_key ${CONSUMER_KEY} "TwidereX" -bundle exec pod keys set consumer_key_secret ${CONSUMER_KEY_SECRET} "TwidereX" -bundle exec pod keys set client_id "" "TwidereX" -bundle exec pod keys set client_id_debug "" "TwidereX" -bundle exec pod keys set host_key_public ${HOST_KEY_PUBLIC} "TwidereX" -bundle exec pod keys set oauth_endpoint ${OAUTH_ENDPOINT} "TwidereX" -bundle exec pod keys set oauth_endpoint_debug "oob" "TwidereX" -bundle exec pod keys set oauth2_endpoint ${OAUTH2_ENDPOINT} "TwidereX" -bundle exec pod keys set oauth2_endpoint_debug ${OAUTH2_ENDPOINT_DEBUG} "TwidereX" -bundle exec pod keys set mastodon_notification_endpoint_debug "" "TwidereX" -bundle exec pod keys set mastodon_notification_endpoint "" "TwidereX" - +bundle exec arkana bundle exec pod install \ No newline at end of file diff --git a/AppShared/AppSecret.swift b/AppShared/AppSecret.swift deleted file mode 100644 index cf94c505..00000000 --- a/AppShared/AppSecret.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// AppSecret.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-9-21. -// - -import Foundation -import CryptoKit -import TwidereCommon -import Keys - -extension AppSecret { - - public convenience init(oauthSecret: OAuthSecret) { - let keys = TwidereXKeys() - self.init( - secret: keys.app_secret, - oauthSecret: oauthSecret, - mastodonNotificationRelayEndpoint: { - #if DEBUG - return keys.mastodon_notification_endpoint_debug - #else - return keys.mastodon_notification_endpoint - #endif - }() - ) - } - - public static let `default`: AppSecret = { - let keys = TwidereXKeys() - let hostPublicKey: Curve25519.KeyAgreement.PublicKey? = { - let keyString = keys.host_key_public - guard let keyData = Data(base64Encoded: keyString), - let key = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData) else { - return nil - } - - return key - }() - - #if DEBUG - let oauthEndpoint = keys.oauth_endpoint_debug - let oauth2Endpoint = keys.oauth2_endpoint_debug - let clientID = keys.client_id_debug - #else - let oauthEndpoint = keys.oauth_endpoint - let oauth2Endpoint = keys.oauth2_endpoint - let clientID = keys.client_id - #endif - - let oauthSecret = AppSecret.OAuthSecret( - consumerKey: keys.consumer_key, - consumerKeySecret: keys.consumer_key_secret, - clientID: clientID, - hostPublicKey: hostPublicKey, - oauthEndpoint: oauthEndpoint, - oauth2Endpoint: oauth2Endpoint - ) - let appSecret = AppSecret(oauthSecret: oauthSecret) - return appSecret - }() - -} - diff --git a/Gemfile b/Gemfile index 48aae3d8..613d835b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" +gem 'arkana' gem "cocoapods" gem "cocoapods-clean" gem "cocoapods-keys" - diff --git a/Gemfile.lock b/Gemfile.lock index d7d0aaf8..75a766af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,6 +17,10 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + arkana (1.1.1) + colorize (~> 0.8) + dotenv (~> 2.7) + yaml (~> 0.2) atomos (0.1.3) claide (1.1.0) cocoapods (1.11.3) @@ -61,6 +65,7 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) + colorize (0.8.1) concurrent-ruby (1.1.10) dotenv (2.7.6) escape (0.0.4) @@ -95,12 +100,14 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + yaml (0.2.0) zeitwerk (2.6.0) PLATFORMS arm64-darwin-21 DEPENDENCIES + arkana cocoapods cocoapods-clean cocoapods-keys diff --git a/Podfile b/Podfile index 7f3df865..f819f6f3 100644 --- a/Podfile +++ b/Podfile @@ -49,24 +49,6 @@ target 'AppShared' do common_pods end -plugin 'cocoapods-keys', { - :project => "TwidereX", - :keys => [ - "app_secret", - "consumer_key", - "consumer_key_secret", - "client_id", - "client_id_debug", - "host_key_public", - "oauth_endpoint", - "oauth_endpoint_debug", - "oauth2_endpoint", - "oauth2_endpoint_debug", - "mastodon_notification_endpoint_debug", - "mastodon_notification_endpoint" - ] -} - post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| diff --git a/Podfile.lock b/Podfile.lock index 78f31965..1c10780e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -95,7 +95,6 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.7.0): - GoogleUtilities/Logger - - Keys (1.0.1) - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) - nanopb/encode (= 2.30909.0) @@ -117,7 +116,6 @@ DEPENDENCIES: - FirebaseMessaging - FirebasePerformance - FLEX (~> 4.4.0) - - Keys (from `Pods/CocoaPodsKeys`) - Sourcery (~> 1.6.1) - SwiftGen (~> 6.3.0) - twitter-text (~> 3.1.0) @@ -150,10 +148,6 @@ SPEC REPOS: - XLPagerTabStrip - ZIPFoundation -EXTERNAL SOURCES: - Keys: - :path: Pods/CocoaPodsKeys - SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 Firebase: 91c243d75573ac6e7c9735ec859b72bffb61347d @@ -171,7 +165,6 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 8ecc717f2abe3f9ee95a5d38db08827852894acc GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 - Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac @@ -180,6 +173,6 @@ SPEC CHECKSUMS: XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: 5e12e332eb31dd3e2519e9f9f18f66b6116e66e1 +PODFILE CHECKSUM: 9151295bd987ef2e612a5a5aa386b5ae12a2fc65 COCOAPODS: 1.11.3 diff --git a/README.md b/README.md index b37aab8f..885b963e 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,8 @@ All you need - Twitter app `API key` and `API key secret` - The latest Xcode from the App Store -- [CocoaPods](https://cocoapods.org) -- [cocoapods-keys](https://github.com/orta/cocoapods-keys) - +- A `.env` file contains the keys. Check the [.env](./env/.env) template and create yours + ```zsh git clone https://github.com/TwidereProject/TwidereX-iOS cd TwidereX-iOS @@ -32,16 +31,13 @@ cd TwidereX-iOS gem install bundle bundle install -bundle exec pod install +# For contributor +bundle exec arkana -e ./env/.env.Homebrew -f Homebrew -# setup cocoapods-keys -> app_secret: "Twidere" -> consumer_key: "" -> consumer_key_secret: "" -> host_key_public: "" -> oauth_endpoint: "oob" -> oauth_endpoint_debug: "oob" +# For App Store release +# bundle exec arkana -e -f AppStore +bundle exec pod install open TwidereX.xcworkspace ``` @@ -58,6 +54,7 @@ The localization resource files locate in [TwidereX-Localization](https://github - [Alamofire](https://github.com/Alamofire/Alamofire) - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) +- [Arkana](https://github.com/rogerluan/arkana) - [cocoapods-keys](https://github.com/orta/cocoapods-keys) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 63ed187f..abc11c82 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -44,6 +44,7 @@ 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(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), + .package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -51,7 +52,8 @@ let package = Package( .target( name: "CoreDataStack", dependencies: [ - "TwidereCommon" + "TwidereCommon", + .product(name: "ArkanaKeys", package: "ArkanaKeys"), ], exclude: [ "Template/Stencil" diff --git a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift index 45ed0987..cd259367 100644 --- a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift +++ b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift @@ -9,6 +9,8 @@ import Foundation import CryptoKit import TwitterSDK import KeychainAccess +import ArkanaKeys +import MastodonSDK public class AppSecret { @@ -53,6 +55,54 @@ public class AppSecret { } +extension AppSecret { + + public convenience init( + oauthSecret: OAuthSecret, + mastodonNotificationEndpoint: String + ) { + self.init( + secret: Keys.Global().appSecret, + oauthSecret: oauthSecret, + mastodonNotificationRelayEndpoint: mastodonNotificationEndpoint + ) + } + + public static let `default`: AppSecret = { + let hostPublicKey: Curve25519.KeyAgreement.PublicKey? = { + let keyString = Keys.Global().hostKeyPublic + guard let keyData = Data(base64Encoded: keyString), + let key = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData) else { + return nil + } + + return key + }() + + #if DEBUG + let keys = Keys.Debug() + #else + let keys = Keys.Release() + #endif + + let oauthSecret = AppSecret.OAuthSecret( + consumerKey: keys.consumerKey, + consumerKeySecret: keys.consumerKeySecret, + clientID: keys.clientID, + hostPublicKey: hostPublicKey, + oauthEndpoint: keys.oauthEndpoint, + oauth2Endpoint: keys.oauth2Endpoint + ) + let appSecret = AppSecret( + oauthSecret: oauthSecret, + mastodonNotificationEndpoint: keys.mastodonNotificationEndpoint + ) + return appSecret + }() + +} + + extension AppSecret { public struct OAuthSecret { public let consumerKey: String diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 28b11a2b..3b260cfd 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -33,7 +33,6 @@ DB0AD4EA28587B040002ABDB /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AD4E928587B040002ABDB /* TimelineViewController.swift */; }; DB0AD4EB28587B520002ABDB /* FederatedTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697E0F279050FC004EF2F7 /* FederatedTimelineViewModel+Diffable.swift */; }; DB0AD4F0285893FA0002ABDB /* TimelineViewModelDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AD4EF285893FA0002ABDB /* TimelineViewModelDriver.swift */; }; - DB0B3B8E26C6639100501BB7 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2611462518AC13004BF309 /* AppSecret.swift */; }; DB0CC4BA27D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0CC4B927D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift */; }; DB12BEE727329F55002AA635 /* SearchUserViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB12BEE627329F55002AA635 /* SearchUserViewController+DataSourceProvider.swift */; }; DB148B00281A729200B596C7 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB148AFF281A729200B596C7 /* SidebarViewController.swift */; }; @@ -562,7 +561,6 @@ DB25C4CE2779A06D00EC1435 /* SavedSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedSearchViewModel.swift; sourceTree = ""; }; DB25C4D02779A37E00EC1435 /* SavedSearchViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SavedSearchViewModel+Diffable.swift"; sourceTree = ""; }; DB25C4D22779ADD800EC1435 /* SearchResultContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultContainerViewController.swift; sourceTree = ""; }; - DB2611462518AC13004BF309 /* AppSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecret.swift; sourceTree = ""; }; DB262A322721377800D18EF3 /* DataSourceFacade+Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Block.swift"; sourceTree = ""; }; DB262A3627213FBE00D18EF3 /* UIAlertAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertAction.swift; sourceTree = ""; }; DB262A382721621800D18EF3 /* DataSourceFacade+User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+User.swift"; sourceTree = ""; }; @@ -1778,7 +1776,6 @@ children = ( DBCDCE602760B8B000F0B78C /* Info.plist */, DB94B6BD26C65CB000A2E8A1 /* AppShared.h */, - DB2611462518AC13004BF309 /* AppSecret.swift */, DBCC7AE7274BA10300E0986D /* Vender */, ); path = AppShared; @@ -2911,7 +2908,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DB0B3B8E26C6639100501BB7 /* AppSecret.swift in Sources */, DBCC7AE5274BA10100E0986D /* DateTimeSwiftProvider.swift in Sources */, DBCC7AE6274BA10100E0986D /* OfficialTwitterTextProvider.swift in Sources */, ); diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 83a5ebad..467aa09a 100644 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -22,21 +22,8 @@ ruby --version # install bundle gem gem install bundle -# setup cocoapods +# setup gems bundle install -# set "TwidereX" project name to make cocoapods-keys using the right project -bundle exec pod keys set app_secret ${APP_SECRET} "TwidereX" -bundle exec pod keys set consumer_key ${CONSUMER_KEY} "TwidereX" -bundle exec pod keys set consumer_key_secret ${CONSUMER_KEY_SECRET} "TwidereX" -bundle exec pod keys set client_id ${CLIENT_ID} "TwidereX" -bundle exec pod keys set client_id_debug ${CLIENT_ID_DEBUG} "TwidereX" -bundle exec pod keys set host_key_public ${HOST_KEY_PUBLIC} "TwidereX" -bundle exec pod keys set oauth_endpoint ${OAUTH_ENDPOINT} "TwidereX" -bundle exec pod keys set oauth_endpoint_debug "oob" "TwidereX" -bundle exec pod keys set oauth2_endpoint ${OAUTH2_ENDPOINT} "TwidereX" -bundle exec pod keys set oauth2_endpoint_debug ${OAUTH2_ENDPOINT_DEBUG} "TwidereX" -bundle exec pod keys set mastodon_notification_endpoint_debug ${MASTODON_NOTIFICATION_ENDPOINT_DEBUG} "TwidereX" -bundle exec pod keys set mastodon_notification_endpoint ${MASTODON_NOTIFICATION_ENDPOINT} "TwidereX" - +bundle exec arkana bundle exec pod install diff --git a/env/.env b/env/.env new file mode 100644 index 00000000..8fae22ef --- /dev/null +++ b/env/.env @@ -0,0 +1,35 @@ +# global +AppSecret="TwidereX" +HostKeyPublic="" + +# Twitter OAuth 1.0a Keys + +# Required +# ConsumerKeyDebug="" +# ConsumerKeyRelease="" + +# Required +# ConsumerKeySecretDebug="" +# ConsumerKeySecretRelease="" + +# Optional: request OAuth on device +OauthEndpointDebug="oob" +OauthEndpointRelease="oob" + +# Twitter OAuth 2.0 Keys +# Optional: override for OAuth2 +ClientIDDebug="" +ClientIDRelease="" + +# Optional: override for OAuth2 +ClientSecretDebug="" +ClientSecretRelease="" + +# Optional: request OAuth on device +Oauth2EndpointDebug="oob" +Oauth2EndpointRelease="oob" + +# Mastodon Push Notification Endpoint +# Optional: +MastodonNotificationEndpointDebug="" +MastodonNotificationEndpointRelease="" \ No newline at end of file From f3327d0b65289839544093fba68eef37e9cdb7c7 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 19:38:49 +0800 Subject: [PATCH 08/38] chore: fix GitHub action env not expose issue --- .github/scripts/setup.sh | 3 +++ .github/workflows/main.yml | 24 +++++++++++++++++------- .github/workflows/release.yml | 21 ++++++++++++++++----- .gitignore | 3 +++ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index 2915c343..43381b65 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -1,5 +1,8 @@ #!/bin/bash +set -xeu +set -o pipefail + gem install bundle bundle install diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9bf0e578..4342225b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,13 +25,23 @@ jobs: - name: setup env: - APP_SECRET: ${{ secrets.APP_SECRET }} - CONSUMER_KEY: ${{ secrets.CONSUMER_KEY }} - CONSUMER_KEY_SECRET: ${{ secrets.CONSUMER_KEY_SECRET }} - HOST_KEY_PUBLIC: ${{ secrets.HOST_KEY_PUBLIC }} - OAUTH_ENDPOINT: ${{ secrets.OAUTH_ENDPOINT }} - OAUTH2_ENDPOINT: ${{ secrets.OAUTH2_ENDPOINT }} - OAUTH2_ENDPOINT_DEBUG: ${{ secrets.OAUTH2_ENDPOINT_DEBUG }} + AppSecret: ${{ secrets.AppSecret }} + HostKeyPublic: ${{ secrets.HostKeyPublic }} + ConsumerKeyDebug: ${{ secrets.ConsumerKeyDebug }} + ConsumerKeyRelease: ${{ secrets.ConsumerKeyRelease }} + ConsumerKeySecretDebug: ${{ secrets.ConsumerKeySecretDebug }} + ConsumerKeySecretRelease: ${{ secrets.ConsumerKeySecretRelease }} + OauthEndpointDebug: ${{ secrets.OauthEndpointDebug }} + OauthEndpointRelease: ${{ secrets.OauthEndpointRelease }} + ClientIDDebug: ${{ secrets.ClientIDDebug }} + ClientIDRelease: ${{ secrets.ClientIDRelease }} + ClientSecretDebug: ${{ secrets.ClientSecretDebug }} + ClientSecretRelease: ${{ secrets.ClientSecretRelease }} + Oauth2EndpointDebug: ${{ secrets.Oauth2EndpointDebug }} + Oauth2EndpointRelease: ${{ secrets.Oauth2EndpointRelease }} + MastodonNotificationEndpointDebug: ${{ secrets.MastodonNotificationEndpointDebug }} + MastodonNotificationEndpointRelease: ${{ secrets.MastodonNotificationEndpointRelease }} + run: exec ./.github/scripts/setup.sh - name: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6279435..e7854a65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,11 +33,22 @@ jobs: - name: setup env: - APP_SECRET: ${{ secrets.APP_SECRET }} - CONSUMER_KEY: ${{ secrets.CONSUMER_KEY }} - CONSUMER_KEY_SECRET: ${{ secrets.CONSUMER_KEY_SECRET }} - HOST_KEY_PUBLIC: ${{ secrets.HOST_KEY_PUBLIC }} - OAUTH_ENDPOINT: ${{ secrets.OAUTH_ENDPOINT }} + AppSecret: ${{ secrets.AppSecret }} + HostKeyPublic: ${{ secrets.HostKeyPublic }} + ConsumerKeyDebug: ${{ secrets.ConsumerKeyDebug }} + ConsumerKeyRelease: ${{ secrets.ConsumerKeyRelease }} + ConsumerKeySecretDebug: ${{ secrets.ConsumerKeySecretDebug }} + ConsumerKeySecretRelease: ${{ secrets.ConsumerKeySecretRelease }} + OauthEndpointDebug: ${{ secrets.OauthEndpointDebug }} + OauthEndpointRelease: ${{ secrets.OauthEndpointRelease }} + ClientIDDebug: ${{ secrets.ClientIDDebug }} + ClientIDRelease: ${{ secrets.ClientIDRelease }} + ClientSecretDebug: ${{ secrets.ClientSecretDebug }} + ClientSecretRelease: ${{ secrets.ClientSecretRelease }} + Oauth2EndpointDebug: ${{ secrets.Oauth2EndpointDebug }} + Oauth2EndpointRelease: ${{ secrets.Oauth2EndpointRelease }} + MastodonNotificationEndpointDebug: ${{ secrets.MastodonNotificationEndpointDebug }} + MastodonNotificationEndpointRelease: ${{ secrets.MastodonNotificationEndpointRelease }} run: exec ./.github/scripts/setup.sh - name: build diff --git a/.gitignore b/.gitignore index c45a497b..846753c1 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ iOSInjectionProject/ **/xcshareddata/WorkspaceSettings.xcsettings # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,cocoapods + +env/**/** +!env/.env \ No newline at end of file From 5129135d0bef1da39290340d28711c1a7a08661b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 19:49:02 +0800 Subject: [PATCH 09/38] chore: update swift-tools-version --- .github/workflows/main.yml | 6 +----- .github/workflows/release.yml | 5 +---- TwidereSDK/Package.swift | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4342225b..f20b87a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,13 +15,10 @@ on: jobs: build: name: CI Build Test - runs-on: macos-11 + runs-on: macos-12 steps: - name: checkout uses: actions/checkout@v2 - - - name: force Xcode 13.2 - run: sudo xcode-select -switch /Applications/Xcode_13.2.app - name: setup env: @@ -41,7 +38,6 @@ jobs: Oauth2EndpointRelease: ${{ secrets.Oauth2EndpointRelease }} MastodonNotificationEndpointDebug: ${{ secrets.MastodonNotificationEndpointDebug }} MastodonNotificationEndpointRelease: ${{ secrets.MastodonNotificationEndpointRelease }} - run: exec ./.github/scripts/setup.sh - name: build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7854a65..eb5ca204 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: push jobs: build: name: Release - runs-on: macos-11 + runs-on: macos-12 if: contains(github.event.head_commit.message, '@r2d2/release') steps: - name: checkout @@ -28,9 +28,6 @@ jobs: api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} - - name: force Xcode 13.2 - run: sudo xcode-select -switch /Applications/Xcode_13.2.app - - name: setup env: AppSecret: ${{ secrets.AppSecret }} diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index abc11c82..195f2cbb 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.6 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From dfe71ac503cfa20a0ce5b5ab65ed04d45107b2c3 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 20:54:32 +0800 Subject: [PATCH 10/38] fix: typo and remove cocoapods-keys --- .github/scripts/setup.sh | 2 +- Gemfile | 1 - Gemfile.lock | 9 --------- ci_scripts/ci_post_clone.sh | 2 +- 4 files changed, 2 insertions(+), 12 deletions(-) mode change 100644 => 100755 ci_scripts/ci_post_clone.sh diff --git a/.github/scripts/setup.sh b/.github/scripts/setup.sh index 43381b65..aa9cd00d 100755 --- a/.github/scripts/setup.sh +++ b/.github/scripts/setup.sh @@ -3,7 +3,7 @@ set -xeu set -o pipefail -gem install bundle +gem install bundler bundle install bundle exec arkana diff --git a/Gemfile b/Gemfile index 613d835b..510d3ccf 100644 --- a/Gemfile +++ b/Gemfile @@ -3,4 +3,3 @@ source "https://rubygems.org" gem 'arkana' gem "cocoapods" gem "cocoapods-clean" -gem "cocoapods-keys" diff --git a/Gemfile.lock b/Gemfile.lock index 75a766af..07356889 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,6 @@ GEM specs: CFPropertyList (3.0.5) rexml - RubyInline (3.12.6) - ZenTest (~> 4.3) - ZenTest (4.12.1) activesupport (6.1.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -54,9 +51,6 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.6.3) - cocoapods-keys (2.2.1) - dotenv - osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -84,8 +78,6 @@ GEM nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) - osx_keychain (1.0.2) - RubyInline (~> 3) public_suffix (4.0.7) rexml (3.2.5) ruby-macho (2.5.1) @@ -110,7 +102,6 @@ DEPENDENCIES arkana cocoapods cocoapods-clean - cocoapods-keys BUNDLED WITH 2.2.32 diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh old mode 100644 new mode 100755 index 467aa09a..ff2256c1 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -20,7 +20,7 @@ rbenv global 3.0.3 ruby --version # install bundle gem -gem install bundle +gem install bundler # setup gems bundle install From 5d6d5e867c9ec1b8d0c86b19eadccb4df970bbdf Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 21:05:06 +0800 Subject: [PATCH 11/38] fix: packet dependency mismatch issue --- TwidereSDK/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index 195f2cbb..a0794b95 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -53,7 +53,6 @@ let package = Package( name: "CoreDataStack", dependencies: [ "TwidereCommon", - .product(name: "ArkanaKeys", package: "ArkanaKeys"), ], exclude: [ "Template/Stencil" @@ -90,6 +89,7 @@ let package = Package( dependencies: [ "TwitterSDK", .product(name: "KeychainAccess", package: "KeychainAccess"), + .product(name: "ArkanaKeys", package: "ArkanaKeys"), ] ), .target( From 14d4b60689c6e751c6f60eaf40149f8498b51ef4 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 21:31:46 +0800 Subject: [PATCH 12/38] fix: wrong Xcode Cloud script working directory issue --- TwidereSDK/Package.swift | 1 + TwidereSDK/Sources/TwidereCommon/AppSecret.swift | 2 +- ci_scripts/ci_post_clone.sh | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index a0794b95..726e31b5 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -88,6 +88,7 @@ let package = Package( name: "TwidereCommon", dependencies: [ "TwitterSDK", + "MastodonSDK", .product(name: "KeychainAccess", package: "KeychainAccess"), .product(name: "ArkanaKeys", package: "ArkanaKeys"), ] diff --git a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift index cd259367..398f4262 100644 --- a/TwidereSDK/Sources/TwidereCommon/AppSecret.swift +++ b/TwidereSDK/Sources/TwidereCommon/AppSecret.swift @@ -8,9 +8,9 @@ import Foundation import CryptoKit import TwitterSDK +import MastodonSDK import KeychainAccess import ArkanaKeys -import MastodonSDK public class AppSecret { diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index ff2256c1..89cd3613 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -8,6 +8,10 @@ set -o pipefail # list hardware system_profiler SPSoftwareDataType SPHardwareDataType +echo $PWD +cd $CI_WORKSPACE +echo $PWD + # install rbenv brew install rbenv which ruby From e237b122243b03bc28bd672d9b2ae537685cdcc2 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 22:17:41 +0800 Subject: [PATCH 13/38] fix: remove Firebase run script to workaround build failure --- TwidereX.xcodeproj/project.pbxproj | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 3b260cfd..c2b2e117 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -2467,7 +2467,6 @@ DBDA8E9A24FE0E4E006750DC /* ShellScript */, DB5632A026DCC52F00FC893F /* ShellScript */, DB51DC3227169C3F00A0D8FB /* ShellScript */, - DB49A09C283422A4000E3B50 /* ShellScript */, DBFC0AEB276118240011E99B /* Embed Frameworks */, DBFDCE0C27F446BB00BE99E3 /* Embed App Extensions */, ); @@ -2811,23 +2810,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DB49A09C283422A4000E3B50 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PODS_ROOT}/FirebaseCrashlytics/run\"\n"; - }; DB51DC3227169C3F00A0D8FB /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2843,7 +2825,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./TwidereX\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + shellScript = "set -xeu\nif [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./TwidereX\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DB5632A026DCC52F00FC893F /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -2860,7 +2842,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./TwidereSDK/Sources/CoreDataStack\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + shellScript = "set -xeu\nif [[ -f \"${PODS_ROOT}/Sourcery/bin/sourcery\" ]]; then\n \"${PODS_ROOT}/Sourcery/bin/sourcery\" --config ./TwidereSDK/Sources/CoreDataStack\nelse\n echo \"warning: Sourcery is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; DBDA8E9A24FE0E4E006750DC /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -2877,7 +2859,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" \nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + shellScript = "set -xeu\nif [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" \nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; F13779F6AF8F9C7C3EBF7183 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; From ef7b71b64f31a495d5f4bbdea38ab11271711123 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 8 Jul 2022 23:34:27 +0800 Subject: [PATCH 14/38] fix: invalid Xcode project info.plist settings --- AppShared/Info.plist | 4 +- Gemfile | 1 + Gemfile.lock | 4 ++ NotificationService/Info.plist | 18 +++++- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 93 +++++------------------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 22 +++++-- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 10 files changed, 61 insertions(+), 89 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 9fe845c6..3fd95d2a 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.0 + 1.4.2 CFBundleVersion - 88 + 111 diff --git a/Gemfile b/Gemfile index 510d3ccf..495f5f49 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,4 @@ source "https://rubygems.org" gem 'arkana' gem "cocoapods" gem "cocoapods-clean" +gem "xcpretty" diff --git a/Gemfile.lock b/Gemfile.lock index 07356889..ed7097fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,6 +80,7 @@ GEM netrc (0.11.0) public_suffix (4.0.7) rexml (3.2.5) + rouge (2.0.7) ruby-macho (2.5.1) typhoeus (1.4.0) ethon (>= 0.9.0) @@ -92,6 +93,8 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.3.0) rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) yaml (0.2.0) zeitwerk (2.6.0) @@ -102,6 +105,7 @@ DEPENDENCIES arkana cocoapods cocoapods-clean + xcpretty BUNDLED WITH 2.2.32 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 3f322a90..47b0cb6e 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -2,10 +2,24 @@ - CFBundleVersion - 110 CFBundleShortVersionString 1.4.2 + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationService + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleVersion + 111 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 96408ea1..a8f866a8 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 110 + 111 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index c2b2e117..da0ff29b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -2370,12 +2370,12 @@ DBFE6EC92876EB42003ABDCB /* NotificationService */ = { isa = PBXGroup; children = ( + DBFE6ECC2876EB42003ABDCB /* Info.plist */, DBFE6ED42876EB4A003ABDCB /* NotificationService.entitlements */, DBFE6ECA2876EB42003ABDCB /* NotificationService.swift */, DBFE6EDC2876F4D2003ABDCB /* NotificationService+Decrypt.swift */, DBFE6EDA2876F47B003ABDCB /* String+Decode85.swift */, DBFE6EDE2876F567003ABDCB /* String+Escape.swift */, - DBFE6ECC2876EB42003ABDCB /* Info.plist */, ); path = NotificationService; sourceTree = ""; @@ -3408,7 +3408,7 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -3436,6 +3436,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 111; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3460,6 +3461,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = PROFILE; SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3474,7 +3476,6 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3487,7 +3488,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3497,7 +3497,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3510,7 +3509,6 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TwidereX.app/TwidereX"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3519,7 +3517,6 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3532,7 +3529,6 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = TwidereX; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3541,16 +3537,14 @@ baseConfigurationReference = 8602431405CD5A4D45224B80 /* Pods-AppShared.profile.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 110; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -3565,17 +3559,14 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; DB2F180B282B54F30001C6A8 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3591,23 +3582,18 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; DB2F180C282B54F30001C6A8 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3619,7 +3605,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; @@ -3628,16 +3613,14 @@ baseConfigurationReference = 5635061D4E24E9A2C69E90ED /* Pods-AppShared.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 110; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -3652,7 +3635,6 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -3661,16 +3643,14 @@ baseConfigurationReference = 06C6C02930FE7CBB4E13D99E /* Pods-AppShared.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 110; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -3684,17 +3664,14 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; DBBBBE6A2744E8CC007ACB4B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3710,17 +3687,14 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; DBBBBE6B2744E8CC007ACB4B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3736,7 +3710,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -3748,7 +3721,7 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -3776,6 +3749,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 111; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3801,6 +3775,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -3812,7 +3787,7 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -3840,6 +3815,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 111; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DISABLE_DIAMOND_PROBLEM_DIAGNOSTIC = YES; ENABLE_NS_ASSERTIONS = NO; @@ -3859,6 +3835,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -3873,7 +3850,6 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3886,7 +3862,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -3901,7 +3876,6 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3914,7 +3888,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -3924,7 +3897,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3937,7 +3909,6 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TwidereX.app/TwidereX"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -3947,7 +3918,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3960,7 +3930,6 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TwidereX.app/TwidereX"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; @@ -3969,7 +3938,6 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3982,7 +3950,6 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = TwidereX; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -3991,7 +3958,6 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4004,23 +3970,18 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = TwidereX; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; DBFDCE4A27F450FC00BE99E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4032,23 +3993,18 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; DBFDCE4B27F450FC00BE99E3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4060,25 +4016,19 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; DBFE6ED02876EB43003ABDCB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; - DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4090,24 +4040,19 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; DBFE6ED12876EB43003ABDCB /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4119,24 +4064,19 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; DBFE6ED22876EB43003ABDCB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 110; DEVELOPMENT_TEAM = 7LFDZ96332; - GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NotificationService; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4148,7 +4088,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 20526939..9fac50de 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 110 + 111 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 8fea4460..78d873da 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -2,10 +2,24 @@ - CFBundleVersion - 110 - CFBundleShortVersionString - 1.4.2 + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + TwidereXIntent + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.4.2 + CFBundleVersion + 111 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 4c2a5f92..3fd95d2a 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 110 + 111 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 4c2a5f92..3fd95d2a 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 110 + 111 From ffa4b3f5f2f7048152a427cfab910e03b5f55ff2 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 9 Jul 2022 00:07:09 +0800 Subject: [PATCH 15/38] chore: use latest Xcode 13.4.1 --- .github/workflows/main.yml | 3 +++ .github/workflows/release.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f20b87a0..c8a45a7d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,9 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 + + - name: force Xcode 13.4.1 + run: sudo xcode-select -switch /Applications/Xcode_13.4.1.app - name: setup env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb5ca204..1a3d7f70 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,9 @@ jobs: api-key-id: ${{ secrets.APPSTORE_KEY_ID }} api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }} + - name: force Xcode 13.4.1 + run: sudo xcode-select -switch /Applications/Xcode_13.4.1.app + - name: setup env: AppSecret: ${{ secrets.AppSecret }} From 077352af8d00654b24408042388870fd16b69ef2 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 9 Jul 2022 01:11:10 +0800 Subject: [PATCH 16/38] chore: remove xcpretty for debug --- .github/scripts/build-debug.sh | 2 +- Podfile.lock | 56 +++++++++++++++++----------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/scripts/build-debug.sh b/.github/scripts/build-debug.sh index 4099c00c..b828368a 100755 --- a/.github/scripts/build-debug.sh +++ b/.github/scripts/build-debug.sh @@ -16,4 +16,4 @@ xcrun xcodebuild \ -parallelizeTargets \ -showBuildTimingSummary \ clean \ - build | xcpretty + build diff --git a/Podfile.lock b/Podfile.lock index 1c10780e..e2776d9c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,41 +1,41 @@ PODS: - DateToolsSwift (5.0.0) - - Firebase/AnalyticsWithoutAdIdSupport (9.1.0): + - Firebase/AnalyticsWithoutAdIdSupport (9.2.0): - Firebase/CoreOnly - - FirebaseAnalytics/WithoutAdIdSupport (~> 9.1.0) - - Firebase/CoreOnly (9.1.0): - - FirebaseCore (= 9.1.0) - - FirebaseABTesting (9.1.0): + - FirebaseAnalytics/WithoutAdIdSupport (~> 9.2.0) + - Firebase/CoreOnly (9.2.0): + - FirebaseCore (= 9.2.0) + - FirebaseABTesting (9.2.0): - FirebaseCore (~> 9.0) - - FirebaseAnalytics/WithoutAdIdSupport (9.1.0): + - FirebaseAnalytics/WithoutAdIdSupport (9.2.0): - FirebaseCore (~> 9.0) - FirebaseInstallations (~> 9.0) - - GoogleAppMeasurement/WithoutAdIdSupport (= 9.1.0) + - GoogleAppMeasurement/WithoutAdIdSupport (= 9.2.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (9.1.0): + - FirebaseCore (9.2.0): - FirebaseCoreDiagnostics (~> 9.0) - FirebaseCoreInternal (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (9.1.0): + - FirebaseCoreDiagnostics (9.2.0): - GoogleDataTransport (< 10.0.0, >= 9.1.4) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCoreInternal (9.1.0): + - FirebaseCoreInternal (9.2.0): - "GoogleUtilities/NSData+zlib (~> 7.7)" - - FirebaseCrashlytics (9.1.0): + - FirebaseCrashlytics (9.2.0): - FirebaseCore (~> 9.0) - FirebaseInstallations (~> 9.0) - GoogleDataTransport (< 10.0.0, >= 9.1.4) - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (9.1.0): + - FirebaseInstallations (9.2.0): - FirebaseCore (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/UserDefaults (~> 7.7) @@ -49,7 +49,7 @@ PODS: - GoogleUtilities/Reachability (~> 7.7) - GoogleUtilities/UserDefaults (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebasePerformance (9.1.0): + - FirebasePerformance (9.2.0): - FirebaseCore (~> 9.0) - FirebaseInstallations (~> 9.0) - FirebaseRemoteConfig (~> 9.0) @@ -58,14 +58,14 @@ PODS: - GoogleUtilities/ISASwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseRemoteConfig (9.1.0): + - FirebaseRemoteConfig (9.2.0): - FirebaseABTesting (~> 9.0) - FirebaseCore (~> 9.0) - FirebaseInstallations (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - FLEX (4.4.1) - - GoogleAppMeasurement/WithoutAdIdSupport (9.1.0): + - GoogleAppMeasurement/WithoutAdIdSupport (9.2.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) @@ -100,7 +100,7 @@ PODS: - nanopb/encode (= 2.30909.0) - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) - - PromisesObjC (2.1.0) + - PromisesObjC (2.1.1) - Sourcery (1.6.1): - Sourcery/CLI-Only (= 1.6.1) - Sourcery/CLI-Only (1.6.1) @@ -150,23 +150,23 @@ SPEC REPOS: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 - Firebase: 91c243d75573ac6e7c9735ec859b72bffb61347d - FirebaseABTesting: 0573c1477dd2fb8204a1d317f8968ea171c6dccc - FirebaseAnalytics: bf11064790ac96804df1df9ddc0ae45af950d9fc - FirebaseCore: b7bfc258e996eada23b3666bd6b9a22ca092acb8 - FirebaseCoreDiagnostics: 2dabba3412b23b524823325739f45e520f67d1e7 - FirebaseCoreInternal: f3c838ae0b41cd840bbf34f90debfede78d352e5 - FirebaseCrashlytics: 8e21736dcf15d814b79229eb7e79ba3a5eec5f30 - FirebaseInstallations: a91c74276b544524ce144c8315d8cdbce2dbe30b + Firebase: 4ba896cb8e5105d4b9e247e1c1b6222b548df55a + FirebaseABTesting: cd1ec762a0078b46a7ce91dfe5b7b8991c2dff8f + FirebaseAnalytics: af5a03a8dff7648c7b8486f6a78b1368e0268dd3 + FirebaseCore: 0e27f2a15d8f7b7ef11e7d93e23b1cbab55d748c + FirebaseCoreDiagnostics: ad3f6c68b7c5b63b7cf15b0785d7137f05f32268 + FirebaseCoreInternal: cb966328b6985dbd6f535e1461291063e1c4a00f + FirebaseCrashlytics: 9fff819edb2bfc9d3eff612225b207d41945a935 + FirebaseInstallations: 21186f0ca7849f90f4a3219fa31a5eca2e30f113 FirebaseMessaging: 4eaf1b8a7464b2c5e619ad66e9b20ee3e3206b24 - FirebasePerformance: c31b7f18df1fda622edd4190d2d6fcececec3e3c - FirebaseRemoteConfig: ebd0eb74b9a0593ab2a3f0788a5be5595b46a05c + FirebasePerformance: 5a8d2a9e645a398dfcc02657853f4b946675d5d4 + FirebaseRemoteConfig: 16e29297f0dd0c7d2415c4506d614fe0b54875d1 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab - GoogleAppMeasurement: 8ecc717f2abe3f9ee95a5d38db08827852894acc + GoogleAppMeasurement: 7a33224321f975d58c166657260526775d9c6b1a GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac SwiftGen: 3d5024a47ea79e408cded20763d7a17d18a4548c twitter-text: 3a0d73ca52955439dc8b208ca7e123ea0abd6a51 From 937a8534c43fbfa3ff3ce8b617299c1b0cafd4e8 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 9 Jul 2022 02:04:42 +0800 Subject: [PATCH 17/38] chore: update Sourcery pod to fix build compatibility issue --- Podfile | 6 ++-- Podfile.lock | 24 +++++++-------- .../TwidereAsset/Generated/Assets.swift | 24 +++++++++++++++ .../Generated/Strings.swift | 6 ++-- TwidereX/Generated/AppIconAssets.swift | 30 ++++++++++++++++++- ...oGenerateTableViewDelegate.generated.swift | 2 +- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/Podfile b/Podfile index f819f6f3..65025ada 100644 --- a/Podfile +++ b/Podfile @@ -25,11 +25,11 @@ target 'TwidereX' do pod 'FirebaseMessaging' # misc - pod 'SwiftGen', '~> 6.3.0' - pod 'Sourcery', '~> 1.6.1' + pod 'SwiftGen', '~> 6.5.1' + pod 'Sourcery', '~> 1.8.1' # Debug - pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] + pod 'FLEX', '~> 4.7.0', :configurations => ['Debug'] pod 'ZIPFoundation', '~> 0.9.11', :configurations => ['Debug'] target 'TwidereXTests' do diff --git a/Podfile.lock b/Podfile.lock index e2776d9c..43dbbccd 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -64,7 +64,7 @@ PODS: - FirebaseInstallations (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - FLEX (4.4.1) + - FLEX (4.7.0) - GoogleAppMeasurement/WithoutAdIdSupport (9.2.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) @@ -101,10 +101,10 @@ PODS: - nanopb/decode (2.30909.0) - nanopb/encode (2.30909.0) - PromisesObjC (2.1.1) - - Sourcery (1.6.1): - - Sourcery/CLI-Only (= 1.6.1) - - Sourcery/CLI-Only (1.6.1) - - SwiftGen (6.3.0) + - Sourcery (1.8.1): + - Sourcery/CLI-Only (= 1.8.1) + - Sourcery/CLI-Only (1.8.1) + - SwiftGen (6.5.1) - twitter-text (3.1.0) - XLPagerTabStrip (9.0.0) - ZIPFoundation (0.9.13) @@ -115,9 +115,9 @@ DEPENDENCIES: - FirebaseCrashlytics - FirebaseMessaging - FirebasePerformance - - FLEX (~> 4.4.0) - - Sourcery (~> 1.6.1) - - SwiftGen (~> 6.3.0) + - FLEX (~> 4.7.0) + - Sourcery (~> 1.8.1) + - SwiftGen (~> 6.5.1) - twitter-text (~> 3.1.0) - XLPagerTabStrip (~> 9.0.0) - ZIPFoundation (~> 0.9.11) @@ -161,18 +161,18 @@ SPEC CHECKSUMS: FirebaseMessaging: 4eaf1b8a7464b2c5e619ad66e9b20ee3e3206b24 FirebasePerformance: 5a8d2a9e645a398dfcc02657853f4b946675d5d4 FirebaseRemoteConfig: 16e29297f0dd0c7d2415c4506d614fe0b54875d1 - FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab + FLEX: bdc9ac7d4a239e3d04c298c01221203257d63a80 GoogleAppMeasurement: 7a33224321f975d58c166657260526775d9c6b1a GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb - Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac - SwiftGen: 3d5024a47ea79e408cded20763d7a17d18a4548c + Sourcery: 4d44d4ea26a682a4a9875ec7c1870a1e7b8e183f + SwiftGen: a6d22010845f08fe18fbdf3a07a8e380fd22e0ea twitter-text: 3a0d73ca52955439dc8b208ca7e123ea0abd6a51 XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5 ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 -PODFILE CHECKSUM: 9151295bd987ef2e612a5a5aa386b5ae12a2fc65 +PODFILE CHECKSUM: 9a116be6704a7b6b4940f5dfb93bf4c4214dbc93 COCOAPODS: 1.11.3 diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index a0dc886e..4079c6cc 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -237,6 +237,17 @@ public final class ColorAsset { return color }() + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = Bundle.module + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + fileprivate init(name: String) { self.name = name } @@ -265,6 +276,7 @@ public struct ImageAsset { public typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) public var image: Image { let bundle = Bundle.module #if os(iOS) || os(tvOS) @@ -280,9 +292,21 @@ public struct ImageAsset { } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + public func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = Bundle.module + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } public extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init?(asset: ImageAsset) { diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 562495f6..1070d871 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -1266,7 +1266,8 @@ public enum L10n { /// Settings public static let title = L10n.tr("Localizable", "Scene.Settings.Title") public enum About { - /// Next generation of Twidere for Android 5.0+. \nStill in early stage. + /// Next generation of Twidere for Android 5.0+. + /// Still in early stage. public static let description = L10n.tr("Localizable", "Scene.Settings.About.Description") /// License public static let license = L10n.tr("Localizable", "Scene.Settings.About.License") @@ -1507,7 +1508,8 @@ public enum L10n { } } public enum SignIn { - /// Hello!\nSign in to Get Started. + /// Hello! + /// Sign in to Get Started. public static let helloSignInToGetStarted = L10n.tr("Localizable", "Scene.SignIn.HelloSignInToGetStarted") /// Sign in with Mastodon public static let signInWithMastodon = L10n.tr("Localizable", "Scene.SignIn.SignInWithMastodon") diff --git a/TwidereX/Generated/AppIconAssets.swift b/TwidereX/Generated/AppIconAssets.swift index 82a07cd9..62c81ef1 100644 --- a/TwidereX/Generated/AppIconAssets.swift +++ b/TwidereX/Generated/AppIconAssets.swift @@ -60,6 +60,17 @@ internal final class ColorAsset { return color }() + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + internal func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = BundleToken.bundle + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + fileprivate init(name: String) { self.name = name } @@ -88,6 +99,7 @@ internal struct ImageAsset { internal typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) internal var image: Image { let bundle = BundleToken.bundle #if os(iOS) || os(tvOS) @@ -103,9 +115,21 @@ internal struct ImageAsset { } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init?(asset: ImageAsset) { @@ -123,7 +147,11 @@ internal extension ImageAsset.Image { // swiftlint:disable convenience_type private final class BundleToken { static let bundle: Bundle = { - Bundle(for: BundleToken.self) + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif }() } // swiftlint:enable convenience_type diff --git a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift index 323a7850..5c657df6 100644 --- a/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift +++ b/TwidereX/Generated/AutoGenerateTableViewDelegate.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 1.6.1 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // sourcery:inline:FederatedTimelineViewController.AutoGenerateTableViewDelegate From 8e7285c79ef0442bf50eeaab020714e16e34e581 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 9 Jul 2022 11:40:02 +0800 Subject: [PATCH 18/38] fix: invisible tableView reload in background always scroll to top issue --- .../Timeline/Base/List/ListTimelineViewController.swift | 7 ++++++- .../Base/List/ListTimelineViewModel+Diffable.swift | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index 71fcfdc6..c57c843d 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -123,7 +123,12 @@ extension ListTimelineViewController { if !viewModel.isLoadingLatest { let now = Date() if let timestamp = viewModel.lastAutomaticFetchTimestamp { - if now.timeIntervalSince(timestamp) > 60 { + #if DEBUG + let throttle: TimeInterval = 1 + #else + let throttle: TimeInterval = 60 + #endif + if now.timeIntervalSince(timestamp) > throttle { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Timeline] auto fetch lastest timeline…") Task { await _viewModel.loadLatest() diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift index 26586ef1..d9bd3b53 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel+Diffable.swift @@ -37,8 +37,13 @@ extension ListTimelineViewModel { ) -> Difference? { guard let sourceIndexPath = (tableView.indexPathsForVisibleRows ?? []).sorted().first else { return nil } let rectForSourceItemCell = tableView.rectForRow(at: sourceIndexPath) - let sourceDistanceToTableViewTopEdge = tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top - + let sourceDistanceToTableViewTopEdge: CGFloat = { + if tableView.window != nil { + return tableView.convert(rectForSourceItemCell, to: nil).origin.y - tableView.safeAreaInsets.top + } else { + return rectForSourceItemCell.origin.y - tableView.contentOffset.y - tableView.safeAreaInsets.top + } + }() guard sourceIndexPath.section < oldSnapshot.numberOfSections, sourceIndexPath.row < oldSnapshot.numberOfItems(inSection: oldSnapshot.sectionIdentifiers[sourceIndexPath.section]) else { return nil } From ab6c954ab46b27ee1778ab7786615bc93a3cd1dd Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 19:27:13 +0800 Subject: [PATCH 19/38] feat: add Mastodon push notification UI/UX logic --- .gitignore | 4 +- Gemfile.lock | 2 +- NotificationService/NotificationService.swift | 12 +- .../CoreDataStack 6.xcdatamodel/contents | 39 ++- .../MastodonAuthentication.swift | 11 + .../MastodonNotificationSubscription.swift | 300 ++++++++++++++++++ ...cationSubscription+MentionPreference.swift | 27 ++ .../MastodonSDK/API/Mastodon+API+Push.swift | 4 +- .../bell.ringing.imageset/Contents.json | 2 +- .../bell.ringing.imageset/bell-ringing.pdf | Bin 0 -> 6805 bytes .../bell.ringing.mini.imageset/Contents.json | 15 + .../bell.ringing.pdf | Bin .../TwidereAsset/Generated/Assets.swift | 1 + .../Preference+NotificationCount.swift | 52 +++ .../TwidereCore/Model/User/UserObject.swift | 14 + .../APIService/APIService+Notification.swift | 40 +-- ...cationService+NotificationSubscriber.swift | 174 ++++++++-- .../Service/NotificationService.swift | 211 +++++++++--- .../TwidereCore/State/AppContext.swift | 4 + .../TwidereCore/State/AuthContext.swift | 20 ++ .../Container/ProfileAvatarView.swift | 4 +- .../Content/UserContentView+ViewModel.swift | 132 ++++++++ .../TwidereUI/Content/UserContentView.swift | 53 ++++ .../Content/UserView+Configuration.swift | 17 +- .../Content/UserView+ViewModel.swift | 25 ++ .../Sources/TwidereUI/Content/UserView.swift | 18 +- .../AvatarButtonRepresentable.swift | 2 +- .../MetaLabelRepresentable.swift | 13 +- .../ProfileAvatarViewRepresentable.swift | 64 ++++ .../Sources/TwitterSDK/API/Twitter+API.swift | 1 - TwidereSDK/Sources/TwitterSDK/Helper.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 51 ++- TwidereX/Coordinator/SceneCoordinator.swift | 13 +- .../Diffable/Misc/TabBar/TabBarItem.swift | 7 + TwidereX/Extension/UIViewController.swift | 47 +++ .../Provider/DataSourceFacade+User.swift | 38 ++- ...ovider+UserViewTableViewCellDelegate.swift | 2 +- .../List/AccountListViewModel+Diffable.swift | 2 +- .../ListUser/ListUserViewModel+Diffable.swift | 2 +- ...tificationTimelineViewModel+Diffable.swift | 2 +- .../NotificationViewController.swift | 6 + .../FollowingListViewModel+Diffable.swift | 2 +- .../Root/ContentSplitViewController.swift | 11 +- .../Drawer/DrawerSidebarViewController.swift | 11 +- .../Root/MainTab/MainTabBarController.swift | 47 +++ TwidereX/Scene/Root/Sidebar/SidebarView.swift | 21 +- .../Scene/Root/Sidebar/SidebarViewModel.swift | 40 +++ .../User/SearchUserViewModel+Diffable.swift | 2 +- .../AccountPreferenceView.swift | 90 ++++++ .../AccountPreferenceViewController.swift | 71 +++++ .../AccountPreferenceViewModel.swift | 70 ++++ .../MastodonNotificationSectionView.swift | 143 +++++++++ ...MastodonNotificationSectionViewModel.swift | 96 ++++++ .../Scene/Setting/List/SettingListView.swift | 42 ++- .../List/SettingListViewController.swift | 17 +- .../Setting/List/SettingListViewModel.swift | 26 +- .../Share/SwiftUI/TableViewEntryRow.swift | 17 +- TwidereX/Supporting Files/AppDelegate.swift | 34 ++ 58 files changed, 2002 insertions(+), 171 deletions(-) create mode 100644 TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift create mode 100644 TwidereSDK/Sources/CoreDataStack/Entity/Transient/Mastodon/MastodonNotificationSubscription+MentionPreference.swift create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/bell-ringing.pdf create mode 100644 TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/Contents.json rename TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/{bell.ringing.imageset => bell.ringing.mini.imageset}/bell.ringing.pdf (100%) create mode 100644 TwidereSDK/Sources/TwidereCommon/Preference/Preference+NotificationCount.swift create mode 100644 TwidereSDK/Sources/TwidereCore/State/AuthContext.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift create mode 100644 TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift create mode 100644 TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ProfileAvatarViewRepresentable.swift create mode 100644 TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift create mode 100644 TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift create mode 100644 TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift create mode 100644 TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift create mode 100644 TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift diff --git a/.gitignore b/.gitignore index 846753c1..23d30acf 100644 --- a/.gitignore +++ b/.gitignore @@ -114,4 +114,6 @@ iOSInjectionProject/ # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,cocoapods env/**/** -!env/.env \ No newline at end of file +!env/.env + +dependencies/ \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index ed7097fa..fbc43764 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,4 +108,4 @@ DEPENDENCIES xcpretty BUNDLED WITH - 2.2.32 + 2.3.17 diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index be5e9c96..0b718d31 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -67,12 +67,12 @@ class NotificationService: UNNotificationServiceExtension { bestAttemptContent.body = notification.body.escape() bestAttemptContent.userInfo["plaintext"] = plaintextData -// let accessToken = notification.accessToken -// UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) -// -// UserDefaults.shared.notificationBadgeCount += 1 -// bestAttemptContent.badge = NSNumber(integerLiteral: UserDefaults.shared.notificationBadgeCount) -// + let accessToken = notification.accessToken + UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) + + UserDefaults.shared.notificationBadgeCount += 1 + bestAttemptContent.badge = NSNumber(integerLiteral: UserDefaults.shared.notificationBadgeCount) + if let urlString = notification.icon, let url = URL(string: urlString) { let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments") try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 6.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 6.xcdatamodel/contents index 883f1e92..69623d81 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 6.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 6.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -35,6 +35,7 @@ + @@ -57,6 +58,23 @@ + + + + + + + + + + + + + + + + + @@ -279,23 +297,4 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/MastodonAuthentication.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/MastodonAuthentication.swift index 7e80ceae..0f7d481c 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/MastodonAuthentication.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Authentication/MastodonAuthentication.swift @@ -36,6 +36,8 @@ final public class MastodonAuthentication: NSManagedObject { // one-to-one relationship @NSManaged public private(set) var authenticationIndex: AuthenticationIndex @NSManaged public private(set) var user: MastodonUser + // sourcery: autoUpdatableObject + @NSManaged public private(set) var notificationSubscription: MastodonNotificationSubscription? } @@ -93,6 +95,10 @@ extension MastodonAuthentication { ]) } + public static func predicate(userAccessToken: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userAccessToken), userAccessToken) + } + } // MARK: - AutoGenerateProperty @@ -180,5 +186,10 @@ extension MastodonAuthentication: AutoUpdatableObject { self.updatedAt = updatedAt } } + public func update(notificationSubscription: MastodonNotificationSubscription?) { + if self.notificationSubscription != notificationSubscription { + self.notificationSubscription = notificationSubscription + } + } // sourcery:end } diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift new file mode 100644 index 00000000..1f1c50a1 --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonNotificationSubscription.swift @@ -0,0 +1,300 @@ +// +// MastodonNotificationSubscription.swift +// +// +// Created by MainasuK on 2022-7-14. +// + +import Foundation +import CoreData + +final public class MastodonNotificationSubscription: NSManagedObject { + public typealias ID = String + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var id: ID? + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public var domain: String? + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var endpoint: String? + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public var serverKey: String? + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public var userToken: String? + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public var isActive: Bool + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var follow: Bool + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var favourite: Bool + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var reblog: Bool + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var mention: Bool + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var poll: Bool + + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var createdAt: Date + // sourcery: autoUpdatableObject, autoGenerateProperty + @NSManaged public private(set) var updatedAt: Date + + // many-to-one relationship + // sourcery: autoGenerateRelationship + @NSManaged public private(set) var authentication: MastodonAuthentication? + +} + +extension MastodonNotificationSubscription { + // sourcery: autoUpdatableObject, autoGenerateProperty + @objc public var mentionPreference: MentionPreference { + get { + let keyPath = #keyPath(MastodonNotificationSubscription.mentionPreference) + willAccessValue(forKey: keyPath) + let _data = primitiveValue(forKey: keyPath) as? Data + didAccessValue(forKey: keyPath) + do { + guard let data = _data, !data.isEmpty else { return MentionPreference() } + let mentionPreference = try JSONDecoder().decode(MentionPreference.self, from: data) + 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) + } + } +} + +extension MastodonNotificationSubscription { + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property, + relationship: Relationship + ) -> MastodonNotificationSubscription { + let object: MastodonNotificationSubscription = context.insertObject() + object.configure(property: property) + object.configure(relationship: relationship) + return object + } + +} + +extension MastodonNotificationSubscription: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \MastodonNotificationSubscription.createdAt, ascending: false)] + } +} + +extension MastodonNotificationSubscription { + + public static func predicate(userToken: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonNotificationSubscription.userToken), userToken) + } + +} + +// MARK: - AutoGenerateProperty +extension MastodonNotificationSubscription: AutoGenerateProperty { + // sourcery:inline:MastodonNotificationSubscription.AutoGenerateProperty + + // Generated using Sourcery + // DO NOT EDIT + public struct Property { + public let id: ID? + public let domain: String? + public let endpoint: String? + public let serverKey: String? + public let userToken: String? + public let isActive: Bool + public let follow: Bool + public let favourite: Bool + public let reblog: Bool + public let mention: Bool + public let poll: Bool + public let createdAt: Date + public let updatedAt: Date + public let mentionPreference: MentionPreference + + public init( + id: ID?, + domain: String?, + endpoint: String?, + serverKey: String?, + userToken: String?, + isActive: Bool, + follow: Bool, + favourite: Bool, + reblog: Bool, + mention: Bool, + poll: Bool, + createdAt: Date, + updatedAt: Date, + mentionPreference: MentionPreference + ) { + self.id = id + self.domain = domain + self.endpoint = endpoint + self.serverKey = serverKey + self.userToken = userToken + self.isActive = isActive + self.follow = follow + self.favourite = favourite + self.reblog = reblog + self.mention = mention + self.poll = poll + self.createdAt = createdAt + self.updatedAt = updatedAt + self.mentionPreference = mentionPreference + } + } + + public func configure(property: Property) { + self.id = property.id + self.domain = property.domain + self.endpoint = property.endpoint + self.serverKey = property.serverKey + self.userToken = property.userToken + self.isActive = property.isActive + self.follow = property.follow + self.favourite = property.favourite + self.reblog = property.reblog + self.mention = property.mention + self.poll = property.poll + self.createdAt = property.createdAt + self.updatedAt = property.updatedAt + self.mentionPreference = property.mentionPreference + } + + public func update(property: Property) { + update(id: property.id) + update(domain: property.domain) + update(endpoint: property.endpoint) + update(serverKey: property.serverKey) + update(userToken: property.userToken) + update(isActive: property.isActive) + update(follow: property.follow) + update(favourite: property.favourite) + update(reblog: property.reblog) + update(mention: property.mention) + update(poll: property.poll) + update(createdAt: property.createdAt) + update(updatedAt: property.updatedAt) + update(mentionPreference: property.mentionPreference) + } + + // sourcery:end +} + +// MARK: - AutoGenerateRelationship +extension MastodonNotificationSubscription: AutoGenerateRelationship { + // sourcery:inline:MastodonNotificationSubscription.AutoGenerateRelationship + + // Generated using Sourcery + // DO NOT EDIT + public struct Relationship { + public let authentication: MastodonAuthentication? + + public init( + authentication: MastodonAuthentication? + ) { + self.authentication = authentication + } + } + + public func configure(relationship: Relationship) { + self.authentication = relationship.authentication + } + + // sourcery:end +} + +// MARK: - AutoUpdatableObject +extension MastodonNotificationSubscription: AutoUpdatableObject { + // sourcery:inline:MastodonNotificationSubscription.AutoUpdatableObject + + // Generated using Sourcery + // DO NOT EDIT + public func update(id: ID?) { + if self.id != id { + self.id = id + } + } + public func update(domain: String?) { + if self.domain != domain { + self.domain = domain + } + } + public func update(endpoint: String?) { + if self.endpoint != endpoint { + self.endpoint = endpoint + } + } + public func update(serverKey: String?) { + if self.serverKey != serverKey { + self.serverKey = serverKey + } + } + public func update(userToken: String?) { + if self.userToken != userToken { + self.userToken = userToken + } + } + public func update(isActive: Bool) { + if self.isActive != isActive { + self.isActive = isActive + } + } + public func update(follow: Bool) { + if self.follow != follow { + self.follow = follow + } + } + public func update(favourite: Bool) { + if self.favourite != favourite { + self.favourite = favourite + } + } + public func update(reblog: Bool) { + if self.reblog != reblog { + self.reblog = reblog + } + } + public func update(mention: Bool) { + if self.mention != mention { + self.mention = mention + } + } + public func update(poll: Bool) { + if self.poll != poll { + self.poll = poll + } + } + public func update(createdAt: Date) { + if self.createdAt != createdAt { + self.createdAt = createdAt + } + } + public func update(updatedAt: Date) { + if self.updatedAt != updatedAt { + self.updatedAt = updatedAt + } + } + public func update(mentionPreference: MentionPreference) { + if self.mentionPreference != mentionPreference { + self.mentionPreference = mentionPreference + } + } + + // sourcery:end +} diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Transient/Mastodon/MastodonNotificationSubscription+MentionPreference.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Transient/Mastodon/MastodonNotificationSubscription+MentionPreference.swift new file mode 100644 index 00000000..03c645db --- /dev/null +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Transient/Mastodon/MastodonNotificationSubscription+MentionPreference.swift @@ -0,0 +1,27 @@ +// +// MastodonNotificationSubscriptionMentionPreference.swift +// +// +// Created by MainasuK on 2022-7-14. +// + +import Foundation + +extension MastodonNotificationSubscription { + public final class MentionPreference: NSObject, Codable { + + public let preference: Preference + + public init(preference: Preference = .everyone) { + self.preference = preference + } + + public enum Preference: String, Codable, CaseIterable, Identifiable, Hashable { + case everyone + case follows + + public var id: Self { self } + } + + } +} diff --git a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift index eb69f012..25aabc7f 100644 --- a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift +++ b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift @@ -188,8 +188,8 @@ extension Mastodon.API.Push { extension Mastodon.API.Push { public struct QuerySubscription: Codable { - let endpoint: String - let keys: Keys + public let endpoint: String + public let keys: Keys public init( endpoint: String, diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/Contents.json index 072db27d..d6a94137 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "bell.ringing.pdf", + "filename" : "bell-ringing.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/bell-ringing.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/bell-ringing.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4ac180ae40f2e0eb8cd8de0e33de941aec491c20 GIT binary patch literal 6805 zcmZvh$&Td45r*%43SI_~4r1rN0D=H%#xe}s@VMoh;6tmXG-{(3ySrt`etLb8B!jFX z>(HqFD>LJdJ(7I)@x%9D*&%nG?M;9E>0e#nfBkj;%{Sfg@=5;f+!}xQ%klF0?U&vI zKFX@sx0mD7xqJ9<{O|MO`1r@~`u9Jq|2w>P|LN>t+NU@3m#_4j{^sZ3%utFe!S{!A zr^~iqUJsWp&wFectj#v&{tOsg4%ziK56*h)YJUREIh*JO^tlAUqMP~An~>rP((?{` zLQdT;U7fc(v&ddRJAb}+P*<9*xF-upGty}tjZ3X6sV|oqQTJM*0mj z7MhjlMTp4Wx=m`*bYvO`k$@@F5TkP?_Ib9cCYU0Jj^GMee4&^olAg3e)G8}^6DG`- z>NLwdFW7Fun%Bv0tFBx{-Wr@V!y5Oh0Zsa=9>j~?D7TbE)ou-`dTXgG{Ul?mm4YCZ zi82r({Q%TEXE7pZ$R@>>gVn}rlVC2IQoza(EXL*=>k8?f#{{mnM$=Yi>s%(&iDfSl z+*1|3N#vgFuBJpeCk@5qLP(7{e9x%aQc8r)bRoJ&$<9V6OvT+H$J9GJI3K)Jl`s^V z7{7zrQcFa;Ks_Rmz3k4}mD1`3=}}6dh=BTtc5S!y-cgg_j6pnwh}|(7m-$u)pPZ#` z!gPm`Fr{dqSC(vOdL=Az?id-w0%A$AK+GZZveH&R+7C+^Qv}1NAvBL*(?AV9NLMc* z*w#QWe&t9rq|DFby3Ef4&4QaV0ol4oM`(I1SB#%^q9nu8W8}~b)|Ah__LLsvMKjun z>MKwTDbvK7F}-a)!((eGmS%Oj8IjV$I@bo_9A`3+<%BeO5${OJ-=QK5mvKX9i}MZW z3q2#yxGYs0I}uTd!bC49gR8Do+*yX=f@4xSba;axqhLbY`4Vzr%)uF!aKtSYgT?|i zEh`eCbS!AEA%DD+wu)I~ORd9n&bDNjWY-~*-D56Hnh+~k5Hqch? zttP)gr?^1LR7h(E5)Gd{#fAKarI6nnKPhV#Dqs ziqS$p&@zK1#8~NYAgRGfMs8zya+UHPe2An8XOiNTEJx8`Oq6J($xOEayg(PA<5KK) z#^lC-hXMTkkc_ADO6lYX;$67X6ih%qr6Mv7BTMF!dB8B465A#~8Itb?V)%o4Q#@L6 zx()QI)^->UZ2M+lD%$O65C0vWjlWJv*kZCE{1w$agn$--U2xgYQr*8s@wVW%!*&yf zuV`3OwJb(Mo4}PT#bB6Xq`M_tC0&g%UgjVw8ZAn^lK454W$8i<)fbKQM3|reY+B(ze z3e!Ff}>Q7tOXaut-@JIJBk z1&C*<;ZqQaH6d1v?3`n5$R#FndkM&mmNEt0TSM4tCTY^07i^tfIxz2oy8E>3USVRD zrRGePs0j+SGD$8ai_Dy=uvRv`P7wcEg#F-sEVWf|t-%aLjDEB{R!-##+KH)}bz0R# zles9``FX$&KQYga>@(n8eYI1NHjJ{awS=jm;OhIQJ~AWCPeZi$pE!@KHi^yJB?4Do zj>9q>Gd7bQjpaHvU1Jv8+|XWKjC845Q654~+xIZtD-5S~D{N&P-7U76RuWh-F$s~N zfqF@%(jq+18Dwl~V-}Z%ft!NdIpmpEERpe%RrDz?r0o%zqa<@_;H=CrlSm&u9XDrM zoFd9$WfaB4CR?dzu*&o$b(F}Vy#>yrT{A<2OtGf3k|L{7iKJRaM(QuU1PvyPvy$Av zK&aMCWt@JmrG&)2HH7U}Dz%ks3I{T5D;1^*OylH|H){YSPb)F_%Dil)jpK*4edRoq zMIYSydxOSVURbaSws3vp}Co69o4vkJ|C)s5wKrl}kIr zl{~l|E#%0uy?eJ@Hgly8-(fg!2B*jUK@<>7!s{OUoixQdG+w{?d5WP zd+mSxC$A#-JpAK-|33B)AD=!SC*ZHgPtQ-^U4QF;TwW-UTkX?3ZL!O@`J~6=>-E={ z!|}D3mmQDOaqq8}x8ntg@}^@3KR&&H^>Kn$??VLA_aqO0e*Og2UNm^@XF2b$zp~eE z_`iazPT~>A>LgL{RRu1JzkhmrI$b~CZvE}$_;bg6(~Vlo-=3Qhg%o)D@TjMK>n{pX zX>Ot9n9uJau4i{pU^cshqFXk;y*xdijxU{f?@u2v#}}{H^YP36i|0>Yjz)QSyk6gW hQMeiS;fLq{A7S5KAU(ajHFXmAweH=!Km6_c-vMB;LUsTE literal 0 HcmV?d00001 diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/Contents.json new file mode 100644 index 00000000..072db27d --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bell.ringing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/bell.ringing.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/bell.ringing.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.imageset/bell.ringing.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.mini.imageset/bell.ringing.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index 4079c6cc..514efa9b 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -151,6 +151,7 @@ public enum Asset { public static let bellLarge = ImageAsset(name: "Object&Tools/bell.large") public static let bellRinging = ImageAsset(name: "Object&Tools/bell.ringing") public static let bellRingingLarge = ImageAsset(name: "Object&Tools/bell.ringing.large") + public static let bellRingingMini = ImageAsset(name: "Object&Tools/bell.ringing.mini") public static let blockedBadge = ImageAsset(name: "Object&Tools/blocked.badge") public static let bookmarks = ImageAsset(name: "Object&Tools/bookmarks") public static let camera = ImageAsset(name: "Object&Tools/camera") diff --git a/TwidereSDK/Sources/TwidereCommon/Preference/Preference+NotificationCount.swift b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+NotificationCount.swift new file mode 100644 index 00000000..d0db1aee --- /dev/null +++ b/TwidereSDK/Sources/TwidereCommon/Preference/Preference+NotificationCount.swift @@ -0,0 +1,52 @@ +// +// Preference+NotificationCount.swift +// +// +// Created by MainasuK on 2022-7-15. +// + +import UIKit +import CryptoKit + +extension UserDefaults { + + // always use hash value (SHA256) from accessToken as key + private static func deriveKey(from accessToken: String, prefix: String) -> String { + let digest = SHA256.hash(data: Data(accessToken.utf8)) + let bytes = [UInt8](digest) + let hex = bytes.toHexString() + let key = prefix + "@" + hex + return key + } + + private static let notificationCountKeyPrefix = "notification_count" + + public func getNotificationCountWithAccessToken(accessToken: String) -> Int { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + return integer(forKey: key) + } + + public func setNotificationCountWithAccessToken(accessToken: String, value: Int) { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + setValue(value, forKey: key) + } + + public func increaseNotificationCount(accessToken: String) { + let count = getNotificationCountWithAccessToken(accessToken: accessToken) + setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1) + } + +} + +extension UserDefaults { + + @objc dynamic public var notificationBadgeCount: Int { + get { + return integer(forKey: #function) + } + set { self[#function] = newValue } + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index 03d7b85c..ab6da105 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -8,6 +8,7 @@ import Foundation import CoreDataStack +import TwidereCommon public enum UserObject: Hashable { case twitter(object: TwitterUser) @@ -32,6 +33,19 @@ extension UserObject { return .mastodon(.init(domain: object.domain, id: object.id)) } } + + public var authenticationContext: AuthenticationContext? { + switch self { + case .twitter(let object): + return object.twitterAuthentication.flatMap { + AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) + } + case .mastodon(let object): + return object.mastodonAuthentication.flatMap { + AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) + } + } + } } extension UserObject { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift index 021288e4..12f01db0 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Notification.swift @@ -12,40 +12,30 @@ import TwidereCommon extension APIService { - func createMastodonNotificationSubscription( + public func createMastodonNotificationSubscription( query: Mastodon.API.Push.CreateSubscriptionQuery, authenticationContext: MastodonAuthenticationContext - ) async throws { - + ) async throws -> Mastodon.Response.Content { let response = try await Mastodon.API.Push.createSubscription( session: session, domain: authenticationContext.domain, query: query, authorization: authenticationContext.authorization ) - -// .flatMap { response -> AnyPublisher, Error> in -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: create subscription successful %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.endpoint) -// -// let managedObjectContext = self.backgroundManagedObjectContext -// return managedObjectContext.performChanges { -// guard let subscription = managedObjectContext.object(with: subscriptionObjectID) as? NotificationSubscription else { -// assertionFailure() -// return -// } -// subscription.endpoint = response.value.endpoint -// subscription.serverKey = response.value.serverKey -// subscription.userToken = authorization.accessToken -// subscription.didUpdate(at: response.networkDate) -// } -// .setFailureType(to: Error.self) -// .map { _ in return response } -// .eraseToAnyPublisher() -// } -// .eraseToAnyPublisher() + + return response + } + + public func cancelMastodonNotificationSubscription( + authenticationContext: MastodonAuthenticationContext + ) async throws -> Mastodon.Response.Content { + return try await cancelMastodonNotificationSubscription( + domain: authenticationContext.domain, + authorization: authenticationContext.authorization + ) } - func cancelMastodonNotificationSubscription( + public func cancelMastodonNotificationSubscription( domain: String, authorization: Mastodon.API.OAuth.Authorization ) async throws -> Mastodon.Response.Content { @@ -55,7 +45,7 @@ extension APIService { authorization: authorization ) - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel subscription successful: \(domain), \(String(describing: authorization))") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel subscription successful: \(domain), user \(String(describing: authorization.accessToken))") return response } diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift index a038cf8b..b990fd9a 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService+NotificationSubscriber.swift @@ -8,9 +8,11 @@ import os.log import Foundation import Combine +import CoreData import CoreDataStack import MastodonSDK import TwidereCommon +import CoreData public struct NotificationSubject { public let fcmToken: String? @@ -32,8 +34,6 @@ extension NotificationService { // input public let userIdentifier: UserIdentifier public let authenticationContext: MastodonAuthenticationContext - - // output init(authenticationContext: MastodonAuthenticationContext) { self.userIdentifier = .mastodon(.init(domain: authenticationContext.domain, id: authenticationContext.userID)) @@ -67,34 +67,158 @@ extension NotificationService.MastodonNotificationSubscriber { api: APIService, subject: NotificationSubject ) async throws { + let authenticationContext = self.authenticationContext + + // precondition: valid fmcToken guard let token = subject.fcmToken else { throw AppError.implicit(.badRequest) } + + // use isolated context to delay object saving + // so update before saving without persistence coordinator sync possible + let managedObjectContext = api.coreDataStack.newTaskContext() + let notificationSubscriptionRecord = try await createOrFetchNotificationSubscription(managedObjectContext: managedObjectContext) - let appSecret = subject.appSecret - let endpoint = appSecret.mastodonNotificationRelayEndpoint + "/" + token - let p256dh = appSecret.mastodonNotificationPublicKey.x963Representation - let auth = appSecret.mastodonNotificationAuth + let subscription: Mastodon.API.Push.QuerySubscription = { + let appSecret = subject.appSecret + let endpoint = appSecret.mastodonNotificationRelayEndpoint + "/" + token + let p256dh = appSecret.mastodonNotificationPublicKey.x963Representation + let auth = appSecret.mastodonNotificationAuth + return .init( + endpoint: endpoint, + keys: .init( + p256dh: p256dh, + auth: auth + ) + ) + }() + + do { + // request server relay push notification on endpoint + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] subscribe notification on \(subscription.endpoint)…") + + let _query: Mastodon.API.Push.CreateSubscriptionQuery? = try await managedObjectContext.perform { + guard let notificationSubscription = notificationSubscriptionRecord.object(in: managedObjectContext) else { + assertionFailure() + throw AppError.implicit(.internal(reason: "precondition: valid subscription record")) + } + guard notificationSubscription.isActive else { + return nil + } + return .init( + subscription: subscription, + data: .init(alerts: .init( + favourite: notificationSubscription.favourite, + follow: notificationSubscription.follow, + reblog: notificationSubscription.reblog, + mention: notificationSubscription.mention, + poll: notificationSubscription.poll + )) + ) + } + guard let query = _query else { + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] cancel notification subscription…") + _ = try await api.cancelMastodonNotificationSubscription( + authenticationContext: authenticationContext + ) + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] cancel notification subscription success") + return + } + + let response = try await api.createMastodonNotificationSubscription( + query: query, + authenticationContext: authenticationContext + ) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] subscribe notification on \(subscription.endpoint) success. Settings:\n\(String(describing: response.value))") + + // update and save the notification subscription object + try? await managedObjectContext.performChanges { + guard let notificationSubscription = notificationSubscriptionRecord.object(in: managedObjectContext) else { + return + } + let subscription = response.value + notificationSubscription.update(domain: authenticationContext.domain) + notificationSubscription.update(id: subscription.id) + notificationSubscription.update(endpoint: subscription.endpoint) + notificationSubscription.update(serverKey: subscription.serverKey) + notificationSubscription.update(userToken: authenticationContext.authorization.accessToken) + notificationSubscription.update(updatedAt: Date()) + + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] update notification subscription") + } + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] subscribe notification on \(subscription.endpoint) failure: \(error.localizedDescription)") + + // save the placeholder notification subscription object + // allow user update preference in settings + // and retry subscribe at the nexta app launch + try? await managedObjectContext.performChanges { + guard let notificationSubscription = notificationSubscriptionRecord.object(in: managedObjectContext) else { + return + } + notificationSubscription.update(updatedAt: Date()) + + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] update notification subscription") + } + + throw error + } + } + + private func createOrFetchNotificationSubscription( + managedObjectContext: NSManagedObjectContext + ) async throws -> ManagedObjectRecord { + let authenticationContext = self.authenticationContext + + var _record: ManagedObjectRecord? = await managedObjectContext.perform { + guard let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext), + let notificationSubscription = authentication.notificationSubscription + else { + return nil + } + return .init(objectID: notificationSubscription.objectID) + } - try await api.createMastodonNotificationSubscription( - query: .init( - subscription: .init( - endpoint: endpoint, - keys: .init( - p256dh: p256dh, - auth: auth - ) - ), - data: .init(alerts: .init( - favourite: true, - follow: true, - reblog: true, - mention: true, - poll: true - )) - ), - authenticationContext: authenticationContext - ) + if _record == nil { + // use perform insert object into context + // but delay the object saving + _record = await managedObjectContext.perform { + guard let authentication = authenticationContext.authenticationRecord.object(in: managedObjectContext) else { + assertionFailure("invalid authentication record") + return nil + } + + let now = Date() + let obejct = MastodonNotificationSubscription.insert( + into: managedObjectContext, + property: .init( + id: nil, + domain: authenticationContext.domain, + endpoint: nil, + serverKey: nil, + userToken: authenticationContext.authorization.accessToken, + isActive: true, + follow: true, + favourite: true, + reblog: true, + mention: true, + poll: true, + createdAt: now, + updatedAt: now, + mentionPreference: MastodonNotificationSubscription.MentionPreference() + ), + relationship: .init(authentication: authentication) + ) + return .init(objectID: obejct.objectID) + } + } + + guard let record = _record else { + assertionFailure() + throw AppError.implicit(.internal(reason: "Cannot create Mastodon notification subscription")) + } + + return record } } diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 698ef0e7..0ad3cd31 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -18,7 +18,7 @@ final public actor NotificationService { var disposeBag = Set() // input - weak var apiService: APIService? + weak var api: APIService? weak var authenticationService: AuthenticationService? let appSecret: AppSecret @@ -30,24 +30,26 @@ final public actor NotificationService { } // output - var subscribers: [NotificationSubscriber] = [] { + public private(set) var subscribers: [NotificationSubscriber] = [] { didSet { notifySubscribers() } } -// let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) -// let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) -// let requestRevealNotificationPublisher = PassthroughSubject() + public let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) + public let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) + public let requestRevealNotificationPublisher = PassthroughSubject() init( apiService: APIService, authenticationService: AuthenticationService, appSecret: AppSecret ) { - self.apiService = apiService + self.api = apiService self.authenticationService = authenticationService self.appSecret = appSecret + // request notification permission if needs + // register notification subscriber authenticationService.$authenticationIndexes .sink { [weak self] authenticationIndexes in guard let self = self else { return } @@ -68,56 +70,92 @@ final public actor NotificationService { } .store(in: &disposeBag) // FIXME: how to use disposeBag in actor under Swift 6 ?? -// Publishers.CombineLatest( -// authenticationService.mastodonAuthentications, -// applicationIconBadgeNeedsUpdate -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] mastodonAuthentications, _ in -// guard let self = self else { return } -// -// var count = 0 -// for authentication in mastodonAuthentications { -// count += UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) -// } -// -// UserDefaults.shared.notificationBadgeCount = count -// UIApplication.shared.applicationIconBadgeNumber = count -// -// self.unreadNotificationCountDidUpdate.send() -// } -// .store(in: &disposeBag) + Publishers.CombineLatest( + authenticationService.$authenticationIndexes, + applicationIconBadgeNeedsUpdate + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authenticationIndexes, _ in + guard let self = self else { return } + + let authenticationContexts = authenticationIndexes.compactMap { authenticationIndex in + AuthenticationContext(authenticationIndex: authenticationIndex, secret: self.appSecret.secret) + } + + var count = 0 + for authenticationContext in authenticationContexts { + switch authenticationContext { + case .twitter: + continue + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let _count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + count += _count + } + } + + UserDefaults.shared.notificationBadgeCount = count + let _count = count + Task { + await self.updateApplicationIconBadge(count: _count) + } + self.unreadNotificationCountDidUpdate.send() + } + .store(in: &disposeBag) } } extension NotificationService { + + public func clearNotificationCountForActiveUser() { + guard let authenticationService = self.authenticationService else { return } + guard let authenticationContext = authenticationService.activeAuthenticationContext else { return } + switch authenticationContext { + case .twitter: + return + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) + } + + applicationIconBadgeNeedsUpdate.send() + } + public func updateToken(_ token: String?) { fcmToken = token } -} + + public func notifySubscriber(authenticationContext: AuthenticationContext) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public). authenticationContext: \(String(describing: authenticationContext))") -extension NotificationService { - private func notifySubscribers() { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - - guard let api = self.apiService else { - assertionFailure() - return - } - + guard let api = self.api else { return } + guard let subscriber = dequeueSubscriber(authenticationContext: authenticationContext) else { return } let subject = NotificationSubject( fcmToken: fcmToken, appSecret: appSecret ) - - for subscriber in subscribers { - subscriber.update(api: api, subject: subject) + subscriber.update(api: api, subject: subject) + } + + public func receive(pushNotification: MastodonPushNotification) async { + defer { + unreadNotificationCountDidUpdate.send() } + + try? await fetchLatestNotifications(pushNotification: pushNotification) + try? await cancelSubscriptionForDetachedAccount(pushNotification: pushNotification) } + } extension NotificationService { + + @MainActor + private func updateApplicationIconBadge(count: Int) { + UIApplication.shared.applicationIconBadgeNumber = count + } + private func requestNotificationPermission() async { do { let center = UNUserNotificationCenter.current() @@ -131,6 +169,28 @@ extension NotificationService { } } +} + +extension NotificationService { + + private func notifySubscribers() { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + guard let api = self.api else { + assertionFailure() + return + } + + let subject = NotificationSubject( + fcmToken: fcmToken, + appSecret: appSecret + ) + + for subscriber in subscribers { + subscriber.update(api: api, subject: subject) + } + } + private func updateSubscribers(_ authenticationContexts: [AuthenticationContext]) async { subscribers = authenticationContexts.compactMap { return dequeueSubscriber(authenticationContext: $0) @@ -153,3 +213,78 @@ extension NotificationService { } } // end func } + + +extension NotificationService { + + private func fetchLatestNotifications( + pushNotification: MastodonPushNotification + ) async throws { + guard let api = self.api else { return } + guard let authenticationContext = try await authenticationContext(for: pushNotification) else { return } + + _ = try await api.mastodonNotificationTimeline( + query: .init(), + scope: .all, + authenticationContext: authenticationContext + ) + } + + private func authenticationContext(for pushNotification: MastodonPushNotification) async throws -> MastodonAuthenticationContext? { + guard let authenticationService = self.authenticationService else { return nil } + let managedObjectContext = authenticationService.managedObjectContext + let _authenticationContext: MastodonAuthenticationContext? = await managedObjectContext.perform { + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken) + request.fetchLimit = 1 + guard let authentication = try? managedObjectContext.fetch(request).first else { return nil } + return MastodonAuthenticationContext(authentication: authentication) + } + return _authenticationContext + } + + private func cancelSubscriptionForDetachedAccount( + pushNotification: MastodonPushNotification + ) async throws { + // Subscription maybe failed to cancel when sign-out + // Try cancel again if receive that kind push notification + guard let api = self.api else { return } + guard let managedObjectContext = authenticationService?.managedObjectContext else { return } + + let userAccessToken = pushNotification.accessToken + + let needsCancelSubscription: Bool = try await managedObjectContext.perform { + // check authentication exists + let authenticationRequest = MastodonAuthentication.sortedFetchRequest + authenticationRequest.predicate = MastodonAuthentication.predicate(userAccessToken: userAccessToken) + return try managedObjectContext.fetch(authenticationRequest).first == nil + } + + guard needsCancelSubscription else { return } + guard let domain = try await domain(for: pushNotification) else { return } + + do { + _ = try await api.cancelMastodonNotificationSubscription( + domain: domain, + authorization: .init(accessToken: userAccessToken) + ) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] cancel sign-out user subscription", ((#file as NSString).lastPathComponent), #line, #function) + } catch { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] failed to cancel sign-out user subscription: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + } + } + + private func domain(for pushNotification: MastodonPushNotification) async throws -> String? { + guard let authenticationService = self.authenticationService else { return nil } + let managedObjectContext = authenticationService.managedObjectContext + return try await managedObjectContext.perform { + let subscriptionRequest = MastodonNotificationSubscription.sortedFetchRequest + subscriptionRequest.predicate = MastodonNotificationSubscription.predicate(userToken: pushNotification.accessToken) + let subscriptions = try managedObjectContext.fetch(subscriptionRequest) + guard let subscription = subscriptions.first else { return nil } + let domain = subscription.domain + return domain + } + } + +} diff --git a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift index 704aff74..2cfce99b 100644 --- a/TwidereSDK/Sources/TwidereCore/State/AppContext.swift +++ b/TwidereSDK/Sources/TwidereCore/State/AppContext.swift @@ -103,4 +103,8 @@ public class AppContext: ObservableObject { } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } diff --git a/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift new file mode 100644 index 00000000..f985704c --- /dev/null +++ b/TwidereSDK/Sources/TwidereCore/State/AuthContext.swift @@ -0,0 +1,20 @@ +// +// AuthContext.swift +// +// +// Created by MainasuK on 2022-7-12. +// + +import Foundation +import CoreData +import CoreDataStack + +public class AuthContext { + + public let authenticationContext: AuthenticationContext + + public init(authenticationContext: AuthenticationContext) { + self.authenticationContext = authenticationContext + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift b/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift index c95cbb27..636d496d 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift @@ -126,14 +126,14 @@ extension ProfileAvatarView { case inline case plain - var primitiveAvatarButtonSize: CGSize { + public var primitiveAvatarButtonSize: CGSize { switch self { case .inline: return CGSize(width: 44, height: 44) case .plain: return CGSize(width: 88, height: 88) } } - var primitiveBadgeImageViewSize: CGSize { + public var primitiveBadgeImageViewSize: CGSize { return CGSize(width: 24, height: 24) } diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift new file mode 100644 index 00000000..5cbda61c --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/UserContentView+ViewModel.swift @@ -0,0 +1,132 @@ +// +// UserContentView+ViewModel.swift +// +// +// Created by MainasuK on 2022-7-12. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import SwiftUI +import TwidereCore +import Meta + +extension UserContentView { + public class ViewModel: ObservableObject { + + // input + public let user: UserObject + public let accessoryType: AccessoryType + + // output + @Published public var platform: Platform = .none + + @Published public var name: MetaContent = PlaintextMetaContent(string: " ") + @Published public var username: MetaContent = PlaintextMetaContent(string: " ") + @Published public var acct: MetaContent = PlaintextMetaContent(string: " ") + @Published public var avatarImageURL: URL? + + @Published public var protected: Bool = false + + public init( + user: UserObject, + accessoryType: AccessoryType + ) { + self.user = user + self.accessoryType = accessoryType + // end init + + configure() + } + } +} + +extension UserContentView.ViewModel { + + public enum AccessoryType { + case none + case disclosureIndicator + } + +} + +extension UserContentView.ViewModel { + + func configure() { + assert(Thread.isMainThread) + + switch user { + case .twitter(let user): + configure(user: user) + case .mastodon(let user): + configure(user: user) + } + } + +} + +extension UserContentView.ViewModel { + private func configure(user: TwitterUser) { + // platform + platform = .twitter + // avatar + user.publisher(for: \.profileImageURL) + .map { _ in user.avatarImageURL() } + .assign(to: &$avatarImageURL) + // author name + user.publisher(for: \.name) + .map { PlaintextMetaContent(string: $0) } + .assign(to: &$name) + // author username + user.publisher(for: \.username) + .map { PlaintextMetaContent(string: "@" + $0) } + .assign(to: &$username) + // acct + user.publisher(for: \.username) + .map { PlaintextMetaContent(string: "@" + $0) } + .assign(to: &$acct) + // protected + user.publisher(for: \.protected) + .assign(to: &$protected) + } +} + +extension UserContentView.ViewModel { + private func configure(user: MastodonUser) { + // platform + platform = .mastodon + // avatar + Publishers.CombineLatest3( + UserDefaults.shared.publisher(for: \.preferredStaticAvatar), + user.publisher(for: \.avatar), + user.publisher(for: \.avatarStatic) + ) + .map { preferredStaticAvatar, avatar, avatarStatic in + let string = preferredStaticAvatar ? (avatarStatic ?? avatar) : avatar + return string.flatMap { URL(string: $0) } + } + .assign(to: &$avatarImageURL) + // author name + Publishers.CombineLatest( + user.publisher(for: \.displayName), + user.publisher(for: \.emojis) + ) + .map { name, _ -> MetaContent in + user.nameMetaContent ?? PlaintextMetaContent(string: name) + } + .assign(to: &$name) + // author username + user.publisher(for: \.acct) + .map { PlaintextMetaContent(string: "@" + $0) } + .assign(to: &$username) + // acct + user.publisher(for: \.acct) + .map { _ in PlaintextMetaContent(string: "@" + user.acctWithDomain) } + .assign(to: &$acct) + // protected + user.publisher(for: \.locked) + .assign(to: &$protected) + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift new file mode 100644 index 00000000..92d416c7 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/UserContentView.swift @@ -0,0 +1,53 @@ +// +// UserContentView.swift +// +// +// Created by MainasuK on 2022-7-12. +// + +import SwiftUI + +public struct UserContentView: View { + + @ObservedObject public var viewModel: ViewModel + + public init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + HStack { + let dimension = ProfileAvatarView.Dimension.inline + ProfileAvatarViewRepresentable( + configuration: .init(url: viewModel.avatarImageURL), + dimension: dimension, + badge: .none + ) + .frame( + width: dimension.primitiveAvatarButtonSize.width, + height: dimension.primitiveAvatarButtonSize.height + ) + VStack(alignment: .leading, spacing: .zero) { + Spacer() + MetaLabelRepresentable( + textStyle: .userAuthorName, + metaContent: viewModel.name + ) + MetaLabelRepresentable( + textStyle: .userAuthorUsername, + metaContent: viewModel.acct + ) + Spacer() + } + Spacer() + switch viewModel.accessoryType { + case .none: + EmptyView() + case .disclosureIndicator: + Image(systemName: "chevron.right") + .foregroundColor(Color(.secondaryLabel)) + } // end switch + } // end HStack + } // end body + +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift index 5c7aa873..6fca1efa 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+Configuration.swift @@ -18,11 +18,11 @@ import MastodonSDK extension UserView { public struct ConfigurationContext { public let listMembershipViewModel: ListMembershipViewModel? - public let authenticationContext: Published.Publisher + public let authenticationContext: AuthenticationContext? public init( listMembershipViewModel: ListMembershipViewModel?, - authenticationContext: Published.Publisher + authenticationContext: AuthenticationContext? ) { self.listMembershipViewModel = listMembershipViewModel self.authenticationContext = authenticationContext @@ -37,9 +37,7 @@ extension UserView { notification: NotificationObject?, configurationContext: ConfigurationContext ) { - configurationContext.authenticationContext - .assign(to: \.authenticationContext, on: viewModel) - .store(in: &disposeBag) + viewModel.authenticationContext = configurationContext.authenticationContext switch user { case .twitter(let user): @@ -97,6 +95,10 @@ extension UserView { viewModel.platform = .twitter // userIdentifier viewModel.userIdentifier = .twitter(.init(id: user.id)) + // userAuthenticationContext + viewModel.userAuthenticationContext = user.twitterAuthentication.flatMap { + AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) + } // avatar user.publisher(for: \.profileImageURL) .map { _ in user.avatarImageURL() } @@ -122,7 +124,6 @@ extension UserView { .assign(to: \.followerCount, on: viewModel) .store(in: &disposeBag) } - } extension UserView { @@ -131,6 +132,10 @@ extension UserView { viewModel.platform = .mastodon // userIdentifier viewModel.userIdentifier = .mastodon(.init(domain: user.domain, id: user.id)) + // userAuthenticationContext + viewModel.userAuthenticationContext = user.mastodonAuthentication.flatMap { + AuthenticationContext(authenticationIndex: $0.authenticationIndex, secret: AppSecret.default.secret) + } // avatar Publishers.CombineLatest3( UserDefaults.shared.publisher(for: \.preferredStaticAvatar), diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index bcc4d594..d54a25f1 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -25,6 +25,7 @@ extension UserView { @Published public var platform: Platform = .none @Published public var authenticationContext: AuthenticationContext? // me + @Published public var userAuthenticationContext: AuthenticationContext? @Published public var header: Header = .none @@ -48,6 +49,8 @@ extension UserView { @Published public var isListMemberCandidate = false // a.k.a isBusy @Published public var isMyList = false + @Published public var badgeCount: Int = 0 + public enum Header { case none case notification(info: NotificationHeaderInfo) @@ -76,6 +79,21 @@ extension UserView { return authenticationContext.userIdentifier == userIdentifier } .assign(to: &$isMyList) + // badge count + $userAuthenticationContext + .map { authenticationContext -> Int in + switch authenticationContext { + case .twitter: + return 0 + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + return count + case .none: + return 0 + } + } + .assign(to: &$badgeCount) } } } @@ -169,6 +187,13 @@ extension UserView.ViewModel { // accessory switch userView.style { case .account: + $badgeCount + .sink { count in + let count = max(0, min(count, 50)) + userView.badgeImageView.image = UIImage(systemName: "\(count).circle.fill")?.withRenderingMode(.alwaysTemplate) + userView.badgeImageView.isHidden = count == 0 + } + .store(in: &disposeBag) userView.menuButton.showsMenuAsPrimaryAction = true userView.menuButton.menu = { let children = [ diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index add41bea..e24dde5a 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -67,6 +67,8 @@ public final class UserView: UIView { public let accessoryContainerView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal + stackView.alignment = .firstBaseline + stackView.spacing = 8 return stackView }() @@ -162,6 +164,13 @@ public final class UserView: UIView { return activityIndicatorView }() + // badge + public let badgeImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.tintColor = .label + return imageView + }() public func prepareForReuse() { disposeBag.removeAll() @@ -263,7 +272,7 @@ extension UserView { public enum Style { // headline: name | lock // subheadline: username - // accessory: menu + // accessory: [ badge | menu ] case account // headline: name | lock | username @@ -279,7 +288,7 @@ extension UserView { // header: notification // headline: name | lock | username // subheadline: follower count - // accessory: [followRquest accept and reject button] + // accessory: [ followRquest accept and reject button ] case notification // headline: name | lock @@ -356,8 +365,11 @@ extension UserView.Style { userView.infoContainerStackView.addArrangedSubview(userView.usernameLabel) + userView.accessoryContainerView.addArrangedSubview(userView.badgeImageView) userView.accessoryContainerView.addArrangedSubview(userView.menuButton) - userView.menuButton.setContentHuggingPriority(.required - 9, for: .horizontal) + userView.badgeImageView.setContentHuggingPriority(.required - 2, for: .horizontal) + userView.badgeImageView.setContentCompressionResistancePriority(.required - 1, for: .vertical) + userView.menuButton.setContentHuggingPriority(.required - 1, for: .horizontal) userView.setNeedsLayout() } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/AvatarButtonRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/AvatarButtonRepresentable.swift index f8e4d954..fde7a3f1 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/AvatarButtonRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/AvatarButtonRepresentable.swift @@ -11,7 +11,7 @@ import TwidereCore public struct AvatarButtonRepresentable: UIViewRepresentable { - let configuration: AvatarImageView.Configuration + public let configuration: AvatarImageView.Configuration public func makeUIView(context: Context) -> AvatarButton { let view = AvatarButton() diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift index d20af8dc..babf6ff1 100644 --- a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/MetaLabelRepresentable.swift @@ -12,11 +12,20 @@ import MetaTextKit public struct MetaLabelRepresentable: UIViewRepresentable { - let textStyle: TextStyle - let metaContent: MetaContent + public let textStyle: TextStyle + public let metaContent: MetaContent + + public init( + textStyle: TextStyle, + metaContent: MetaContent + ) { + self.textStyle = textStyle + self.metaContent = metaContent + } public func makeUIView(context: Context) -> MetaLabel { let view = MetaLabel(style: textStyle) + view.isUserInteractionEnabled = false return view } diff --git a/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ProfileAvatarViewRepresentable.swift b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ProfileAvatarViewRepresentable.swift new file mode 100644 index 00000000..6a7ac2cd --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/UIViewRepresentable/ProfileAvatarViewRepresentable.swift @@ -0,0 +1,64 @@ +// +// ProfileAvatarViewRepresentable.swift +// +// +// Created by MainasuK on 2022-7-12. +// + +import UIKit +import SwiftUI +import TwidereCore + +public struct ProfileAvatarViewRepresentable: UIViewRepresentable { + + public let configuration: AvatarImageView.Configuration + public let dimension: ProfileAvatarView.Dimension + public let badge: ProfileAvatarView.Badge + + public init( + configuration: AvatarImageView.Configuration, + dimension: ProfileAvatarView.Dimension, + badge: ProfileAvatarView.Badge + ) { + self.configuration = configuration + self.dimension = dimension + self.badge = badge + } + + public func makeUIView(context: Context) -> ProfileAvatarView { + let view = ProfileAvatarView() + view.setup(dimension: dimension) + view.badge = badge + return view + } + + public func updateUIView(_ view: ProfileAvatarView, context: Context) { + view.avatarButton.avatarImageView.configure(configuration: configuration) + } + +} + +#if DEBUG +struct ProfileAvatarViewRepresentable_Preview: PreviewProvider { + static var previews: some View { + Group { + ProfileAvatarViewRepresentable( + configuration: .init(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")), + dimension: .inline, + badge: .robot + ) + .frame(width: 44, height: 44, alignment: .center) + .previewLayout(.fixed(width: 44, height: 44)) + ProfileAvatarViewRepresentable( + configuration: .init(url: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")), + dimension: .plain, + badge: .circle(.mastodon) + ) + .frame(width: 88, height: 88, alignment: .center) + .previewLayout(.fixed(width: 88, height: 88)) + } + } +} +#endif + + diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift index ad89cf08..5848150c 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API.swift @@ -53,7 +53,6 @@ extension Twitter.API { } - extension Twitter.API { enum Method: String { diff --git a/TwidereSDK/Sources/TwitterSDK/Helper.swift b/TwidereSDK/Sources/TwitterSDK/Helper.swift index 531c3517..0bf9991f 100644 --- a/TwidereSDK/Sources/TwitterSDK/Helper.swift +++ b/TwidereSDK/Sources/TwitterSDK/Helper.swift @@ -1,5 +1,5 @@ // -// File.swift +// Helper.swift // // // Created by Cirno MainasuK on 2020-10-16. diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index da0ff29b..80e08c2b 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -170,6 +170,9 @@ DB6BCD72277AEED600847054 /* TrendViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6BCD71277AEED600847054 /* TrendViewModel+Diffable.swift */; }; DB6CF1C4269715CD001DE069 /* FPSIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DB6CF1C3269715CD001DE069 /* FPSIndicator */; }; DB6DF3E0252060AA00E8A273 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6DF3DF252060AA00E8A273 /* ProfileViewModel.swift */; }; + DB6E7800287D65520099FAE4 /* AccountPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6E77FF287D65520099FAE4 /* AccountPreferenceViewController.swift */; }; + DB6E7803287D65DB0099FAE4 /* AccountPreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6E7802287D65DB0099FAE4 /* AccountPreferenceViewModel.swift */; }; + DB6E7805287D85090099FAE4 /* AccountPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6E7804287D85090099FAE4 /* AccountPreferenceView.swift */; }; DB71C7D1271EB09A00BE3819 /* DataSourceFacade+Friendship.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7D0271EB09A00BE3819 /* DataSourceFacade+Friendship.swift */; }; DB71C7D3271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7D2271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift */; }; DB7274EF273BB25200577D95 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274EE273BB25200577D95 /* NotificationViewController.swift */; }; @@ -318,6 +321,8 @@ DBE71B7D26B7C5FD00DFAB8E /* StubTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */; }; DBE71B7F26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */; }; DBE76D1E2500E65D00DEB0FC /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE76D1D2500E65D00DEB0FC /* HomeTimelineViewModel.swift */; }; + DBEA1B6F287FFB0B00BE55D0 /* MastodonNotificationSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA1B6E287FFB0B00BE55D0 /* MastodonNotificationSectionView.swift */; }; + DBEA1B72287FFEF000BE55D0 /* MastodonNotificationSectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA1B71287FFEF000BE55D0 /* MastodonNotificationSectionViewModel.swift */; }; DBEA4F842511F7460007FEC5 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = DBEA4F832511F7460007FEC5 /* Kanna */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */; }; @@ -681,6 +686,9 @@ DB6BCD6F277AEAC700847054 /* TrendTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendTableViewCell.swift; sourceTree = ""; }; DB6BCD71277AEED600847054 /* TrendViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrendViewModel+Diffable.swift"; sourceTree = ""; }; DB6DF3DF252060AA00E8A273 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; + DB6E77FF287D65520099FAE4 /* AccountPreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPreferenceViewController.swift; sourceTree = ""; }; + DB6E7802287D65DB0099FAE4 /* AccountPreferenceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPreferenceViewModel.swift; sourceTree = ""; }; + DB6E7804287D85090099FAE4 /* AccountPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPreferenceView.swift; sourceTree = ""; }; DB71C7D0271EB09A00BE3819 /* DataSourceFacade+Friendship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Friendship.swift"; sourceTree = ""; }; DB71C7D2271EB71800BE3819 /* ProfileViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB7274EE273BB25200577D95 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; @@ -859,6 +867,8 @@ 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 = ""; }; + DBEA1B6E287FFB0B00BE55D0 /* MastodonNotificationSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonNotificationSectionView.swift; sourceTree = ""; }; + DBEA1B71287FFEF000BE55D0 /* MastodonNotificationSectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonNotificationSectionViewModel.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 = ""; }; @@ -1204,6 +1214,7 @@ isa = PBXGroup; children = ( DB2C8739274F4B7D00CE0398 /* List */, + DB6E7801287D65550099FAE4 /* AccountPreference */, DBF81C8127F6E84300004A56 /* Appearance */, DB2C8733274F4B7D00CE0398 /* DisplayPreference */, DB2C872F274F4B7D00CE0398 /* Developer */, @@ -1215,9 +1226,9 @@ DB2C872F274F4B7D00CE0398 /* Developer */ = { isa = PBXGroup; children = ( - DB2C8730274F4B7D00CE0398 /* DeveloperView.swift */, - DB2C8731274F4B7D00CE0398 /* DeveloperViewModel.swift */, DB2C8732274F4B7D00CE0398 /* DeveloperViewController.swift */, + DB2C8731274F4B7D00CE0398 /* DeveloperViewModel.swift */, + DB2C8730274F4B7D00CE0398 /* DeveloperView.swift */, ); path = Developer; sourceTree = ""; @@ -1244,9 +1255,9 @@ DB2C8739274F4B7D00CE0398 /* List */ = { isa = PBXGroup; children = ( - DB2C873A274F4B7D00CE0398 /* SettingListView.swift */, - DB235EF32834DD0900398FCA /* SettingListViewModel.swift */, DB2C873B274F4B7D00CE0398 /* SettingListViewController.swift */, + DB235EF32834DD0900398FCA /* SettingListViewModel.swift */, + DB2C873A274F4B7D00CE0398 /* SettingListView.swift */, ); path = List; sourceTree = ""; @@ -1557,6 +1568,17 @@ path = Trend; sourceTree = ""; }; + DB6E7801287D65550099FAE4 /* AccountPreference */ = { + isa = PBXGroup; + children = ( + DB6E77FF287D65520099FAE4 /* AccountPreferenceViewController.swift */, + DB6E7802287D65DB0099FAE4 /* AccountPreferenceViewModel.swift */, + DB6E7804287D85090099FAE4 /* AccountPreferenceView.swift */, + DBEA1B70287FFE3100BE55D0 /* MastodonNotification */, + ); + path = AccountPreference; + sourceTree = ""; + }; DB7274F0273BB25A00577D95 /* Notification */ = { isa = PBXGroup; children = ( @@ -2204,6 +2226,15 @@ path = Extension; sourceTree = ""; }; + DBEA1B70287FFE3100BE55D0 /* MastodonNotification */ = { + isa = PBXGroup; + children = ( + DBEA1B71287FFEF000BE55D0 /* MastodonNotificationSectionViewModel.swift */, + DBEA1B6E287FFB0B00BE55D0 /* MastodonNotificationSectionView.swift */, + ); + path = MastodonNotification; + sourceTree = ""; + }; DBEA4F782511E4660007FEC5 /* Share */ = { isa = PBXGroup; children = ( @@ -2312,9 +2343,9 @@ children = ( DBF81C9227F8449800004A56 /* AppIcon */, DBF81C8627F709F200004A56 /* Translation */, - DBF81C7F27F6E80700004A56 /* AppearanceView.swift */, - DBF81C8227F6ECF400004A56 /* AppearanceViewModel.swift */, DBF81C7D27F6E7F500004A56 /* AppearanceViewController.swift */, + DBF81C8227F6ECF400004A56 /* AppearanceViewModel.swift */, + DBF81C7F27F6E80700004A56 /* AppearanceView.swift */, ); path = Appearance; sourceTree = ""; @@ -2986,6 +3017,7 @@ DB51DC412717FCAA00A0D8FB /* StatusMediaGalleryCollectionCell.swift in Sources */, DB9B3251285735F200AC818D /* GridTimelineViewController.swift in Sources */, DB3B906126E8AB480010F64C /* StatusViewTableViewCellDelegate.swift in Sources */, + DBEA1B72287FFEF000BE55D0 /* MastodonNotificationSectionViewModel.swift in Sources */, DB46D11A27DB26FF003B8BA1 /* ListUserViewController.swift in Sources */, DB1E48122772CE380074F6A0 /* SearchViewModel.swift in Sources */, DB76A662275F65FF00A50673 /* DataSourceFacade+Model.swift in Sources */, @@ -2994,9 +3026,11 @@ DB8761CB2745530200BA7EE2 /* CoverFlowStackSection.swift in Sources */, DB8AC0F825401BA200E636BE /* UIViewController.swift in Sources */, DB434888251DFE2D005B599F /* ProfileBannerStatusView.swift in Sources */, + DB6E7800287D65520099FAE4 /* AccountPreferenceViewController.swift in Sources */, DB442475285B17830095AECF /* SearchMediaTimelineViewModel+Diffable.swift in Sources */, DBBBBE8F2744FB42007ACB4B /* ComposeViewModel.swift in Sources */, DB761E62255942050050DC01 /* DrawerSidebarEntryView.swift in Sources */, + DBEA1B6F287FFB0B00BE55D0 /* MastodonNotificationSectionView.swift in Sources */, DB92570A251C8FE0004FEFB5 /* ProfileHeaderViewController.swift in Sources */, DB47AB1D27CCB88000CD73C7 /* ListViewModel.swift in Sources */, DB8761BF274552F800BA7EE2 /* StatusItem.swift in Sources */, @@ -3142,6 +3176,7 @@ DBDAF244274F530B00050319 /* NeedsDependency.swift in Sources */, DB8761C3274552FB00BA7EE2 /* HashtagSection.swift in Sources */, DB9B325C2857493D00AC818D /* UserTimelineViewModel+Diffable.swift in Sources */, + DB6E7805287D85090099FAE4 /* AccountPreferenceView.swift in Sources */, DBF81C8E27F843D700004A56 /* AppIconAssets.swift in Sources */, DB262A3B272288FC00D18EF3 /* SearchViewController.swift in Sources */, DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */, @@ -3205,6 +3240,7 @@ DB76A66A27606F6900A50673 /* MediaPreviewVideoViewController.swift in Sources */, DB148B0C281A837E00B596C7 /* SidebarView.swift in Sources */, DBC8E050257653E100401E20 /* SavePhotoActivity.swift in Sources */, + DB6E7803287D65DB0099FAE4 /* AccountPreferenceViewModel.swift in Sources */, DB1D7B4325B5938400397DCD /* TwitterAuthenticationOptionViewController.swift in Sources */, DB830209273D12E600BF5224 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */, DB8761C1274552F800BA7EE2 /* UserItem.swift in Sources */, @@ -3476,6 +3512,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3850,6 +3887,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3876,6 +3914,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index fb1d0c31..cfb00a41 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -86,7 +86,8 @@ extension SceneCoordinator { case searchResult(viewModel: SearchResultViewModel) // Settings - case setting + case setting(viewModel: SettingListViewModel) + case accountPreference(viewModel: AccountPreferenceViewModel) case appearance case displayPreference case about @@ -333,8 +334,14 @@ private extension SceneCoordinator { _viewController.searchResultViewModel = viewModel _viewController.searchResultViewController = searchResultViewController viewController = _viewController - case .setting: - viewController = SettingListViewController() + case .setting(let viewModel): + let _viewController = SettingListViewController() + _viewController.viewModel = viewModel + viewController = _viewController + case .accountPreference(let viewModel): + let _viewController = AccountPreferenceViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .appearance: viewController = AppearanceViewController() case .displayPreference: diff --git a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift index fc72e911..d4934b6e 100644 --- a/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift +++ b/TwidereX/Diffable/Misc/TabBar/TabBarItem.swift @@ -66,6 +66,13 @@ extension TabBarItem { } } + var altImage: UIImage { + switch self { + case .notification: return Asset.ObjectTools.bellRinging.image.withRenderingMode(.alwaysTemplate) + default: return image + } + } + var largeImage: UIImage { return image.resized(size: CGSize(width: 80, height: 80)) } diff --git a/TwidereX/Extension/UIViewController.swift b/TwidereX/Extension/UIViewController.swift index bfff3854..9695e480 100644 --- a/TwidereX/Extension/UIViewController.swift +++ b/TwidereX/Extension/UIViewController.swift @@ -47,6 +47,53 @@ extension UIViewController { } +extension UIViewController { + + func viewController(of type: T.Type) -> T? { + if let viewController = self as? T { + return viewController + } + + // UITabBarController + if let tabBarController = self as? UITabBarController { + for tab in tabBarController.viewControllers ?? [] { + if let viewController = tab.viewController(of: type) { + return viewController + } + } + } + + // UINavigationController + if let navigationController = self as? UINavigationController { + for page in navigationController.viewControllers { + if let viewController = page.viewController(of: type) { + return viewController + } + } + } + + // UIPageController + if let pageViewController = self as? UIPageViewController { + for page in pageViewController.viewControllers ?? [] { + if let viewController = page.viewController(of: type) { + return viewController + } + } + } + + // child view controller + for subview in self.view?.subviews ?? [] { + if let childViewController = subview.next as? UIViewController, + let viewController = childViewController.viewController(of: type) { + return viewController + } + } + + return nil + } + +} + extension UIViewController { /// https://bluelemonbits.com/2018/08/26/inserting-cells-at-the-top-of-a-uitableview-with-no-scrolling/ diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift index 488683ad..c1137e77 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+User.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+User.swift @@ -313,7 +313,7 @@ extension DataSourceFacade { extension DataSourceFacade { @MainActor static func responseToUserSignOut( - provider: DataSourceProvider, + dependency: NeedsDependency & UIViewController, user: UserRecord ) async throws { let alertController = UIAlertController( @@ -324,12 +324,34 @@ extension DataSourceFacade { let signOutAction = UIAlertAction( title: L10n.Common.Controls.Actions.signOut, style: .destructive - ) { [weak provider] _ in - guard let provider = provider else { return } + ) { [weak dependency] _ in + guard let dependency = dependency else { return } Task { var isSignOut = false - let managedObjectContext = provider.context.backgroundManagedObjectContext + // clear badge before sign-out + await dependency.context.notificationService.clearNotificationCountForActiveUser() + + // cancel push notification subscription + do { + let _authenticationContext: AuthenticationContext? = await dependency.context.managedObjectContext.perform { + guard let user = user.object(in: dependency.context.managedObjectContext) else { return nil } + guard let authenticationContext = user.authenticationContext else { return nil } + return authenticationContext + } + switch _authenticationContext { + case .twitter: + break + case .mastodon(let authenticationContext): + _ = try await dependency.context.apiService.cancelMastodonNotificationSubscription(authenticationContext: authenticationContext) + case .none: + break + } + } catch { + // do nothing + } + + let managedObjectContext = dependency.context.backgroundManagedObjectContext try await managedObjectContext.performChanges { guard let object = user.object(in: managedObjectContext) else { return } switch object { @@ -351,16 +373,16 @@ extension DataSourceFacade { } guard isSignOut else { return } - provider.coordinator.setup() - provider.coordinator.setupWelcomeIfNeeds() + dependency.coordinator.setup() + dependency.coordinator.setupWelcomeIfNeeds() } // end Task } alertController.addAction(signOutAction) let cancelAction = UIAlertAction.cancel alertController.addAction(cancelAction) - provider.coordinator.present( + dependency.coordinator.present( scene: .alertController(alertController: alertController), - from: provider, + from: dependency, transition: .alertController(animated: true, completion: nil) ) } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift index 3aec4d29..48802925 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UserViewTableViewCellDelegate.swift @@ -33,7 +33,7 @@ extension UserViewTableViewCellDelegate where Self: DataSourceProvider { return } try await DataSourceFacade.responseToUserSignOut( - provider: self, + dependency: self, user: user ) } // end Task diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index ca9f8157..b3b30e6d 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -24,7 +24,7 @@ extension AccountListViewModel { userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( listMembershipViewModel: nil, - authenticationContext: context.authenticationService.$activeAuthenticationContext + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) ) diff --git a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift index fd58cbf2..aeb36a5c 100644 --- a/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift +++ b/TwidereX/Scene/List/ListUser/ListUserViewModel+Diffable.swift @@ -23,7 +23,7 @@ extension ListUserViewModel { userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( listMembershipViewModel: listMembershipViewModel, - authenticationContext: context.authenticationService.$activeAuthenticationContext + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) ) diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index ed8b9868..fec95e2e 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -31,7 +31,7 @@ extension NotificationTimelineViewModel { ), userViewConfigurationContext: .init( listMembershipViewModel: nil, - authenticationContext: context.authenticationService.$activeAuthenticationContext + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) diffableDataSource = NotificationSection.diffableDataSource( diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 95e3c974..520e984f 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -11,6 +11,7 @@ import UIKit import Combine import Tabman import Pageboy +import TwidereCore final class NotificationViewController: TabmanViewController, NeedsDependency, DrawerSidebarTransitionHostViewController { @@ -120,6 +121,11 @@ extension NotificationViewController { super.viewDidAppear(animated) viewModel.viewDidAppear.send() + + // reset notification count + Task { + await self.context.notificationService.clearNotificationCountForActiveUser() + } // end Task } } diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift index b4c49cee..c9a880b5 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+Diffable.swift @@ -21,7 +21,7 @@ extension FriendshipListViewModel { userViewTableViewCellDelegate: nil, userViewConfigurationContext: .init( listMembershipViewModel: nil, - authenticationContext: context.authenticationService.$activeAuthenticationContext + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 79ba2a45..3c05458f 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -216,7 +216,16 @@ extension ContentSplitViewController: SidebarViewModelDelegate { switch tab { case .settings: - coordinator.present(scene: .setting, from: nil, transition: .modal(animated: true, completion: nil)) + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } + let settingListViewModel = SettingListViewModel( + context: context, + auth: .init(authenticationContext: authenticationContext) + ) + coordinator.present( + scene: .setting(viewModel: settingListViewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) default: viewModel.activeTab = tab if mainTabBarController.tabs.contains(tab) { diff --git a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift index b2e172c1..f28b1d51 100644 --- a/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Root/Drawer/DrawerSidebarViewController.swift @@ -235,8 +235,17 @@ extension DrawerSidebarViewController: UICollectionViewDelegate { guard let diffableDataSource = viewModel.settingDiffableDataSource else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard case .settings = item else { return } + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { return } dismiss(animated: true) { - self.coordinator.present(scene: .setting, from: nil, transition: .modal(animated: true, completion: nil)) + let settingListViewModel = SettingListViewModel( + context: self.context, + auth: .init(authenticationContext: authenticationContext) + ) + self.coordinator.present( + scene: .setting(viewModel: settingListViewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) } default: assertionFailure() diff --git a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift index ef2a38f4..5c61cf99 100644 --- a/TwidereX/Scene/Root/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/Root/MainTab/MainTabBarController.swift @@ -77,6 +77,7 @@ extension MainTabBarController { let feedbackGenerator = UINotificationFeedbackGenerator() + // post publish result observer context.publisherService.statusPublishResult .receive(on: DispatchQueue.main) .sink { [weak self] result in @@ -124,6 +125,10 @@ extension MainTabBarController { } .store(in: &disposeBag) + Task { + await setupNotificationTabIconUpdater() + } // end Task + let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) @@ -150,6 +155,11 @@ extension MainTabBarController { } extension MainTabBarController { + + private var notificationViewController: NotificationViewController? { + return viewController(of: NotificationViewController.self) + } + private func updateTabBarDisplay() { switch traitCollection.horizontalSizeClass { case .compact: @@ -158,6 +168,43 @@ extension MainTabBarController { tabBar.isHidden = true } } + + @MainActor + private func setupNotificationTabIconUpdater() async { + // notification tab bar icon updater + await Publishers.CombineLatest3( + context.authenticationService.$activeAuthenticationContext, + context.notificationService.unreadNotificationCountDidUpdate, // <-- actor property + $currentTab + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authenticationContext, _, currentTab in + guard let self = self else { return } + guard let authenticationContext = authenticationContext else { return } + guard let notificationViewController = self.notificationViewController else { return } + + let hasUnreadPushNotification: Bool = { + switch authenticationContext { + case .twitter: + return false + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + return count > 0 + } + }() + let image = hasUnreadPushNotification ? Asset.ObjectTools.bellRinging.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectTools.bell.image.withRenderingMode(.alwaysTemplate) + let largeImage = hasUnreadPushNotification ? Asset.ObjectTools.bellRingingLarge.image.withRenderingMode(.alwaysTemplate) : Asset.ObjectTools.bellLarge.image.withRenderingMode(.alwaysTemplate) + + notificationViewController.tabBarItem.image = image + notificationViewController.tabBarItem.largeContentSizeImage = largeImage + + notificationViewController.navigationController?.tabBarItem.image = image + notificationViewController.navigationController?.tabBarItem.largeContentSizeImage = largeImage + } + .store(in: &disposeBag) + } + } extension MainTabBarController { diff --git a/TwidereX/Scene/Root/Sidebar/SidebarView.swift b/TwidereX/Scene/Root/Sidebar/SidebarView.swift index d2927d1a..7671ad36 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarView.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarView.swift @@ -15,12 +15,22 @@ struct SidebarView: View { @ObservedObject var viewModel: SidebarViewModel + func shouldUseAltStyle(for item: TabBarItem) -> Bool { + switch item { + case .notification: + return viewModel.hasUnreadPushNotification + default: + return false + } + } + var body: some View { VStack(spacing: .zero) { ForEach(viewModel.mainTabBarItems, id: \.self) { item in EntryButton( item: item, - isActive: viewModel.activeTab == item + isActive: viewModel.activeTab == item, + useAltStyle: shouldUseAltStyle(for: item) ) { item in viewModel.setActiveTab(item: item) } @@ -37,7 +47,8 @@ struct SidebarView: View { ForEach(viewModel.secondaryTabBarItems, id: \.self) { item in EntryButton( item: item, - isActive: viewModel.activeTab == item + isActive: viewModel.activeTab == item, + useAltStyle: shouldUseAltStyle(for: item) ) { item in viewModel.setActiveTab(item: item) } @@ -50,7 +61,8 @@ struct SidebarView: View { Spacer() EntryButton( item: .settings, - isActive: false + isActive: false, + useAltStyle: false ) { item in viewModel.setActiveTab(item: item) } @@ -67,6 +79,7 @@ extension SidebarView { struct EntryButton: View { let item: TabBarItem let isActive: Bool + let useAltStyle: Bool let action: (TabBarItem) -> () var body: some View { @@ -76,7 +89,7 @@ extension SidebarView { action(item) } label: { VectorImageView( - image: item.image, + image: useAltStyle ? item.altImage : item.image, tintColor: isActive ? .tintColor : .secondaryLabel ) .frame(width: dimension, height: dimension, alignment: .center) diff --git a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift index 2bde5ef8..5bfa3449 100644 --- a/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/TwidereX/Scene/Root/Sidebar/SidebarViewModel.swift @@ -10,6 +10,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import TwidereCore +import TwidereAsset protocol SidebarViewModelDelegate: AnyObject { func sidebarViewModel(_ viewModel: SidebarViewModel, active item: TabBarItem) @@ -30,6 +32,8 @@ final class SidebarViewModel: ObservableObject { @Published var mainTabBarItems: [TabBarItem] = [] @Published var secondaryTabBarItems: [TabBarItem] = [] @Published var avatarURL: URL? + + @Published var hasUnreadPushNotification = false init(context: AppContext) { self.context = context @@ -68,6 +72,10 @@ final class SidebarViewModel: ObservableObject { } } .store(in: &disposeBag) + + Task { + await setupNotificationTabIconUpdater() + } // end Task } } @@ -79,3 +87,35 @@ extension SidebarViewModel { } } + +extension SidebarViewModel { + + @MainActor + private func setupNotificationTabIconUpdater() async { + // notification tab bar icon updater + await Publishers.CombineLatest3( + context.authenticationService.$activeAuthenticationContext, + context.notificationService.unreadNotificationCountDidUpdate, // <-- actor property + $activeTab + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authenticationContext, _, activeTab in + guard let self = self else { return } + guard let authenticationContext = authenticationContext else { return } + + let hasUnreadPushNotification: Bool = { + switch authenticationContext { + case .twitter: + return false + case .mastodon(let authenticationContext): + let accessToken = authenticationContext.authorization.accessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + return count > 0 + } + }() + self.hasUnreadPushNotification = hasUnreadPushNotification + } + .store(in: &disposeBag) + } + +} diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift index 7e6fedd8..28ec4d84 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+Diffable.swift @@ -26,7 +26,7 @@ extension SearchUserViewModel { userViewTableViewCellDelegate: userViewTableViewCellDelegate, userViewConfigurationContext: .init( listMembershipViewModel: listMembershipViewModel, - authenticationContext: context.authenticationService.$activeAuthenticationContext + authenticationContext: context.authenticationService.activeAuthenticationContext ) ) ) diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift new file mode 100644 index 00000000..f735903a --- /dev/null +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift @@ -0,0 +1,90 @@ +// +// AccountPreferenceView.swift +// TwidereX +// +// Created by MainasuK on 2022-7-12. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import SwiftUI +import TwidereUI + +enum AccountPreferenceListEntry: Hashable { + case muted + case blocked + case accountSettings + case signout + + var title: String { + switch self { + case .muted: return "Muted People" + case .blocked: return "Blocked People" + case .accountSettings: return "Account Settings" + case .signout: return L10n.Common.Controls.Actions.signOut + } + } +} + +struct AccountPreferenceView: View { + + @ObservedObject var viewModel: AccountPreferenceViewModel + + @State var isPushNotificationEnabled = true + + var muteAndBlockSection: some View { + Section(header: Text("Mute and Block")) { + let entries: [AccountPreferenceListEntry] = [ + .muted, .blocked + ] + ForEach(entries, id: \.self) { entry in + Button { + viewModel.listEntryPublisher.send(entry) + } label: { + TableViewEntryRow(icon: nil, title: entry.title) + .foregroundColor(Color(.label)) + } + } + } + } + + + var body: some View { + List { + // user header section + Section { + UserContentView(viewModel: .init( + user: viewModel.user, + accessoryType: .none + )) + } + // notification section + if let viewModel = viewModel.mastodonNotificationSectionViewModel { + MastodonNotificationSectionView(viewModel: viewModel) + } + // mute & block section + // muteAndBlockSection + // account settings secton + // Section { + // let entry = AccountPreferenceListEntry.accountSettings + // Button { + // viewModel.listEntryPublisher.send(entry) + // } label: { + // TableViewEntryRow(icon: nil, title: entry.title, accessorySymbolName: "arrow.up.right.square") + // .foregroundColor(Color(.label)) + // } + // } + // sign out section + Section { + let entry = AccountPreferenceListEntry.signout + Button { + viewModel.listEntryPublisher.send(entry) + } label: { + TableViewEntryRow(icon: nil, title: entry.title, accessorySymbolName: nil) + .foregroundColor(Color(uiColor: .systemRed)) + } + } + } // end List + } + +} diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift new file mode 100644 index 00000000..2e810c5a --- /dev/null +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewController.swift @@ -0,0 +1,71 @@ +// +// AccountPreferenceViewController.swift +// TwidereX +// +// Created by MainasuK on 2022-7-12. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import SwiftUI +import Combine +import TwidereCore + +final class AccountPreferenceViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + + var viewModel: AccountPreferenceViewModel! + private(set) lazy var accountPreferenceView = AccountPreferenceView(viewModel: viewModel) + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension AccountPreferenceViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + let hostingController = UIHostingController(rootView: accountPreferenceView) + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + hostingController.didMove(toParent: self) + + viewModel.listEntryPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] entry in + guard let self = self else { return } + switch entry { + case .muted: + break + case .blocked: + break + case .accountSettings: + break + case .signout: + Task { + try await DataSourceFacade.responseToUserSignOut( + dependency: self, + user: self.viewModel.user.asRecord + ) + } // end Task + } + } + .store(in: &disposeBag) + } + +} diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift new file mode 100644 index 00000000..c7c64fa4 --- /dev/null +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceViewModel.swift @@ -0,0 +1,70 @@ +// +// AccountPreferenceViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-12. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import SwiftUI +import Combine +import CoreDataStack +import TwidereCore + +final class AccountPreferenceViewModel: ObservableObject { + + // input + let context: AppContext + let auth: AuthContext + let user: UserObject + + // notification + @Published var mastodonNotificationSectionViewModel: MastodonNotificationSectionViewModel? + @Published var isNewFollowEnabled = true + @Published var isReblogEnabled = true + @Published var isFavoriteEnabled = true + @Published var isPollEnabled = true + @Published var isMentionEnabled = true + + // output + let listEntryPublisher = PassthroughSubject() + + init( + context: AppContext, + auth: AuthContext, + user: UserObject + ) { + self.context = context + self.auth = auth + self.user = user + // end init + + setupNotificationSource() + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension AccountPreferenceViewModel { + func setupNotificationSource() { + switch user { + case .twitter: + // do nothing + break + case .mastodon(let user): + mastodonNotificationSectionViewModel = user.mastodonAuthentication?.notificationSubscription.flatMap { + return .init( + context: context, + auth: auth, + notificationSubscription: $0 + ) + } + } + } +} + diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift new file mode 100644 index 00000000..f10e7434 --- /dev/null +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift @@ -0,0 +1,143 @@ +// +// MastodonNotificationSectionView.swift +// TwidereX +// +// Created by MainasuK on 2022-7-14. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import SwiftUI +import CoreDataStack +import SwiftMessages + +struct MastodonNotificationSectionView: View { + + @ObservedObject var viewModel: MastodonNotificationSectionViewModel + + var body: some View { + Group { + // push notification secion + Section { + Toggle("Push Notification", isOn: Binding( + get: { viewModel.isActive }, + set: { newValue in + viewModel.isActive = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(isActive: newValue) + } + } + )) + } + // notification option section + if viewModel.isActive { + notificationOptionSection + #if DEBUG + // // mention option section + // if viewModel.isMentionEnabled { + // mentionPreferenceSection + // } + #endif + } + } // end Group + } // end body + +} + +extension MastodonNotificationSectionView { + + var notificationOptionSection: some View { + Section(header: Text("Notifications")) { + Toggle("New Follow", isOn: Binding( + get: { viewModel.isNewFollowEnabled }, + set: { newValue in + viewModel.isNewFollowEnabled = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(follow: newValue) + } + } + )) + Toggle("Reblog", isOn: Binding( + get: { viewModel.isReblogEnabled }, + set: { newValue in + viewModel.isReblogEnabled = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(reblog: newValue) + } + } + )) + Toggle("Favorite", isOn: Binding( + get: { viewModel.isFavoriteEnabled }, + set: { newValue in + viewModel.isFavoriteEnabled = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(favourite: newValue) + } + } + )) + Toggle("Poll", isOn: Binding( + get: { viewModel.isPollEnabled }, + set: { newValue in + viewModel.isPollEnabled = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(poll: newValue) + } + } + )) + Toggle("Mention", isOn: Binding( + get: { viewModel.isMentionEnabled }, + set: { newValue in + viewModel.isMentionEnabled = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + notificationSubscription.update(mention: newValue) + } + } + )) + } // end Section + } + + var mentionPreferenceSection: some View { + Section( + header: Text("Notifications: Mention"), + footer: + Group { + switch viewModel.mentionPreference { + case .everyone: + EmptyView() + case .follows: + Text("Only mentions from your following account will be received.") + .font(.footnote) + .foregroundColor(.secondary) + } + } // end Group + ) { + Picker(selection: Binding( + get: { viewModel.mentionPreference }, + set: { newValue in + viewModel.mentionPreference = newValue + viewModel.updateNotificationSubscription { notificationSubscription in + let mentionPreference = MastodonNotificationSubscription.MentionPreference(preference: newValue) + notificationSubscription.update(mentionPreference: mentionPreference) + } + } + )) { + ForEach(MastodonNotificationSubscription.MentionPreference.Preference.allCases) { prefence in + Text(prefence.title) + } + } label: { + EmptyView() + } + .pickerStyle(.inline) + } // end Section + } + +} + +extension MastodonNotificationSubscription.MentionPreference.Preference { + fileprivate var title: String { + switch self { + case .everyone: return "Everyone" + case .follows: return "Follows" + } + } +} diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift new file mode 100644 index 00000000..0f1c3f7d --- /dev/null +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionViewModel.swift @@ -0,0 +1,96 @@ +// +// MastodonNotificationSectionViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-14. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import Foundation +import CoreDataStack +import TwidereCore + +final class MastodonNotificationSectionViewModel: ObservableObject { + + // input + let context: AppContext + let auth: AuthContext + let notificationSubscription: MastodonNotificationSubscription + + // output + @Published var isActive: Bool + + @Published var isNewFollowEnabled: Bool + @Published var isReblogEnabled: Bool + @Published var isFavoriteEnabled: Bool + @Published var isPollEnabled: Bool + @Published var isMentionEnabled: Bool + + @Published var mentionPreference: MastodonNotificationSubscription.MentionPreference.Preference + + init( + context: AppContext, + auth: AuthContext, + notificationSubscription: MastodonNotificationSubscription + ) { + self.context = context + self.auth = auth + self.notificationSubscription = notificationSubscription + self.isActive = notificationSubscription.isActive + self.isNewFollowEnabled = notificationSubscription.follow + self.isReblogEnabled = notificationSubscription.reblog + self.isFavoriteEnabled = notificationSubscription.favourite + self.isPollEnabled = notificationSubscription.poll + self.isMentionEnabled = notificationSubscription.mention + self.mentionPreference = notificationSubscription.mentionPreference.preference + // end init + + notificationSubscription.publisher(for: \.isActive) + .receive(on: DispatchQueue.main) + .assign(to: &$isActive) + + notificationSubscription.publisher(for: \.follow) + .receive(on: DispatchQueue.main) + .assign(to: &$isNewFollowEnabled) + notificationSubscription.publisher(for: \.reblog) + .receive(on: DispatchQueue.main) + .assign(to: &$isReblogEnabled) + notificationSubscription.publisher(for: \.favourite) + .receive(on: DispatchQueue.main) + .assign(to: &$isFavoriteEnabled) + notificationSubscription.publisher(for: \.poll) + .receive(on: DispatchQueue.main) + .assign(to: &$isPollEnabled) + notificationSubscription.publisher(for: \.mention) + .receive(on: DispatchQueue.main) + .assign(to: &$isMentionEnabled) + + notificationSubscription.publisher(for: \.mentionPreference) + .receive(on: DispatchQueue.main) + .map { $0.preference } + .assign(to: &$mentionPreference) + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension MastodonNotificationSectionViewModel { + + func updateNotificationSubscription(action: @escaping (MastodonNotificationSubscription) -> Void) { + let record = ManagedObjectRecord(objectID: notificationSubscription.objectID) + Task { + let managedObjectContext = context.coreDataStack.newTaskContext() + try await managedObjectContext.performChanges { + guard let object = record.object(in: managedObjectContext) else { return } + action(object) + } + + await context.notificationService.notifySubscriber(authenticationContext: auth.authenticationContext) + } // end Task + } + +} diff --git a/TwidereX/Scene/Setting/List/SettingListView.swift b/TwidereX/Scene/Setting/List/SettingListView.swift index 76112f39..97633ad4 100644 --- a/TwidereX/Scene/Setting/List/SettingListView.swift +++ b/TwidereX/Scene/Setting/List/SettingListView.swift @@ -6,7 +6,10 @@ // Copyright © 2020 Twidere. All rights reserved. // +import CoreData +import CoreDataStack import SwiftUI +import TwidereUI struct TextCaseEraseStyle: ViewModifier { func body(content: Content) -> some View { @@ -21,6 +24,7 @@ struct TextCaseEraseStyle: ViewModifier { } enum SettingListEntryType: Hashable { + case account case appearance case display case layout @@ -33,6 +37,7 @@ enum SettingListEntryType: Hashable { var image: Image { switch self { + case .account: return Image(systemName: "person") case .appearance: return Image(uiImage: Asset.ObjectTools.clothes.image) case .display: return Image(uiImage: Asset.TextFormatting.textHeaderRedaction.image) case .layout: return Image(uiImage: Asset.sidebarLeft.image) @@ -46,6 +51,7 @@ enum SettingListEntryType: Hashable { var title: String { switch self { + case .account: return "Account" // TODO: i18n case .appearance: return L10n.Scene.Settings.Appearance.title case .display: return L10n.Scene.Settings.Display.title case .layout: return "Layout" @@ -69,6 +75,23 @@ struct SettingListView: View { @EnvironmentObject var context: AppContext @ObservedObject var viewModel: SettingListViewModel + + static let accountListEntry: SettingListEntry = { + let type = SettingListEntryType.account + return SettingListEntry(type: type, image: type.image, title: type.title) + }() + + @ViewBuilder + var accountView: some View { + if let user = viewModel.user { + UserContentView(viewModel: .init( + user: user, + accessoryType: .disclosureIndicator + )) + } else { + EmptyView() + } + } static let generalSection: [SettingListEntry] = { let types: [SettingListEntryType] = [ @@ -104,6 +127,13 @@ struct SettingListView: View { var body: some View { List { + Section(header: Text(verbatim: "Account")) { + Button { + viewModel.settingListEntryPublisher.send(SettingListView.accountListEntry) + } label: { + accountView + } + } Section( // grouped tableView get header padding since iOS 15. // no more top padding manually @@ -155,9 +185,15 @@ struct SettingListView: View { struct SettingListView_Previews: PreviewProvider { static var previews: some View { Group { - SettingListView(viewModel: SettingListViewModel()) - SettingListView(viewModel: SettingListViewModel()) - .preferredColorScheme(.dark) + SettingListView(viewModel: SettingListViewModel( + context: .shared, + auth: nil + )) + SettingListView(viewModel: SettingListViewModel( + context: .shared, + auth: nil + )) + .preferredColorScheme(.dark) } } } diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index 8981c789..10eb07b3 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -17,7 +17,7 @@ final class SettingListViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - let viewModel = SettingListViewModel() + var viewModel: SettingListViewModel! } @@ -48,6 +48,21 @@ extension SettingListViewController { .sink { [weak self] entry in guard let self = self else { return } switch entry.type { + case .account: + // FIXME: + guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } + guard let user = self.viewModel.user else { return } + + let accountPreferenceViewModel = AccountPreferenceViewModel( + context: self.context, + auth: .init(authenticationContext: authenticationContext), + user: user + ) + self.coordinator.present( + scene: .accountPreference(viewModel: accountPreferenceViewModel), + from: self, + transition: .show + ) case .appearance: self.coordinator.present(scene: .appearance, from: self, transition: .show) case .display: diff --git a/TwidereX/Scene/Setting/List/SettingListViewModel.swift b/TwidereX/Scene/Setting/List/SettingListViewModel.swift index 37e53967..a8820053 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewModel.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewModel.swift @@ -10,20 +10,44 @@ import os.log import Foundation import SwiftUI import Combine +import CoreDataStack import SwiftyJSON import TwitterSDK +import TwidereCore +import Meta final class SettingListViewModel: ObservableObject { var disposeBag = Set() // input + let context: AppContext + let auth: AuthContext? // output let settingListEntryPublisher = PassthroughSubject() - init() { + // account + @Published var user: UserObject? + + init( + context: AppContext, + auth: AuthContext? + ) { + self.context = context + self.auth = auth + // end init + Task { + await setupAccountSource() + } } } + +extension SettingListViewModel { + @MainActor + func setupAccountSource() async { + user = auth?.authenticationContext.user(in: context.managedObjectContext) + } +} diff --git a/TwidereX/Scene/Share/SwiftUI/TableViewEntryRow.swift b/TwidereX/Scene/Share/SwiftUI/TableViewEntryRow.swift index ca0253e2..712f39b2 100644 --- a/TwidereX/Scene/Share/SwiftUI/TableViewEntryRow.swift +++ b/TwidereX/Scene/Share/SwiftUI/TableViewEntryRow.swift @@ -12,6 +12,17 @@ struct TableViewEntryRow: View { let icon: Image? let title: String + let accessorySymbolName: String? + + init( + icon: Image? = nil, + title: String, + accessorySymbolName: String? = "chevron.right" + ) { + self.icon = icon + self.title = title + self.accessorySymbolName = accessorySymbolName + } var body: some View { HStack { @@ -20,8 +31,10 @@ struct TableViewEntryRow: View { } Text(title) Spacer() - Image(systemName: "chevron.right") - .foregroundColor(Color(.secondaryLabel)) + if let name = accessorySymbolName { + Image(systemName: name) + .foregroundColor(Color(.secondaryLabel)) + } } } diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index 48dc7af0..f7a4f7ee 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -97,6 +97,40 @@ extension AppDelegate { // MARK: - UNUserNotificationCenterDelegate extension AppDelegate: UNUserNotificationCenterDelegate { + // notification present in the foreground + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push]", ((#file as NSString).lastPathComponent), #line, #function) + guard let pushNotification = AppDelegate.mastodonPushNotification(from: notification) else { + completionHandler([]) + return + } + + let notificationID = String(pushNotification.notificationID) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + + let accessToken = pushNotification.accessToken + UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) + Task { + await self.appContext.notificationService.applicationIconBadgeNeedsUpdate.send() + await self.appContext.notificationService.receive(pushNotification: pushNotification) + } // end Task + + completionHandler([.sound]) + } + + private static func mastodonPushNotification(from notification: UNNotification) -> MastodonPushNotification? { + guard let plaintext = notification.request.content.userInfo["plaintext"] as? Data, + let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext) else { + return nil + } + + return mastodonPushNotification + } + } // MARK: - MessagingDelegate From cb48d7ba9aac70f8a86db551f75f3b65185e93f8 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 15 Jul 2022 19:37:38 +0800 Subject: [PATCH 20/38] chore: update version to 1.4.2 (111) --- ShareExtension/Info.plist | 4 +-- TwidereX.xcodeproj/project.pbxproj | 51 ++++++++++++++++-------------- TwidereX/Info.plist | 4 +-- TwidereXTests/Info.plist | 4 +-- TwidereXUITests/Info.plist | 4 +-- 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 7427c176..a8f866a8 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 109 + 111 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index ab3dda31..ac0d3c98 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3413,7 +3413,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3435,9 +3435,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = D286181E8236434CBB750613 /* Pods-TwidereXTests.profile.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3445,7 +3446,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3460,7 +3461,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3486,11 +3487,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 109; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3518,7 +3519,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3545,7 +3546,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3575,11 +3576,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 109; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3609,11 +3610,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 109; + DYLIB_CURRENT_VERSION = 111; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3640,7 +3641,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3667,7 +3668,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3821,7 +3822,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3850,7 +3851,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3872,9 +3873,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3CFF05E953F47DC2B0B5AC95 /* Pods-TwidereXTests.debug.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3882,7 +3884,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3896,9 +3898,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2504A7AE733D038B91122765 /* Pods-TwidereXTests.release.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3906,7 +3909,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3921,7 +3924,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3944,7 +3947,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3968,7 +3971,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3996,7 +3999,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 109; + CURRENT_PROJECT_VERSION = 111; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 3ef7ab2a..9fac50de 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 109 + 111 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 7c504fbc..3fd95d2a 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 109 + 111 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 7c504fbc..3fd95d2a 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 109 + 111 From 8254227f7cd57e8b84383b050e52a21999d4cef7 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 18 Jul 2022 21:49:12 +0800 Subject: [PATCH 21/38] feat: add deep link for Mastodon push notification --- NotificationService/NotificationService.swift | 1 - .../Utility/ManagedObjectRecord.swift | 6 + .../MastodonPushNotification.swift | 2 +- .../TwidereCore/Model/User/UserObject.swift | 9 + .../APIService+Timeline+Notification.swift | 33 +++ .../Service/NotificationService.swift | 2 +- TwidereX.xcodeproj/project.pbxproj | 273 ++++++++++++++++-- TwidereX/Coordinator/SceneCoordinator.swift | 144 ++++++++- .../Root/ContentSplitViewController.swift | 6 + .../AppIconPreferenceView.swift | 0 .../AppearancePreferenceView.swift} | 6 +- .../AppearancePreferenceViewController.swift} | 15 +- .../AppearancePreferenceViewModel.swift} | 4 +- .../TranslateButtonPreferenceView.swift | 0 .../TranslationServicePreferenceView.swift | 0 .../List/SettingListViewController.swift | 2 +- .../PushNotificationScratchView.swift | 38 +++ ...ushNotificationScratchViewController.swift | 104 +++++++ .../PushNotificationScratchViewModel.swift | 34 +++ ...meTimelineViewController+DebugAction.swift | 12 + TwidereX/Supporting Files/AppDelegate.swift | 26 +- 21 files changed, 674 insertions(+), 43 deletions(-) rename TwidereX/Scene/Setting/{Appearance => AppearancePreference}/AppIconPreferenceView.swift (100%) rename TwidereX/Scene/Setting/{Appearance/AppearanceView.swift => AppearancePreference/AppearancePreferenceView.swift} (92%) rename TwidereX/Scene/Setting/{Appearance/AppearanceViewController.swift => AppearancePreference/AppearancePreferenceViewController.swift} (75%) rename TwidereX/Scene/Setting/{Appearance/AppearanceViewModel.swift => AppearancePreference/AppearancePreferenceViewModel.swift} (92%) rename TwidereX/Scene/Setting/{Appearance => AppearancePreference}/TranslateButtonPreferenceView.swift (100%) rename TwidereX/Scene/Setting/{Appearance => AppearancePreference}/TranslationServicePreferenceView.swift (100%) create mode 100644 TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchView.swift create mode 100644 TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewController.swift create mode 100644 TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 0b718d31..81fca613 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -101,7 +101,6 @@ class NotificationService: UNNotificationServiceExtension { } else { contentHandler(bestAttemptContent) } - } } diff --git a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift index dbdce6c3..de4915be 100644 --- a/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift +++ b/TwidereSDK/Sources/CoreDataStack/Utility/ManagedObjectRecord.swift @@ -30,3 +30,9 @@ public class ManagedObjectRecord: Hashable { } } + +extension Managed where Self: NSManagedObject { + public var asRecrod: ManagedObjectRecord { + return .init(objectID: objectID) + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift index 17f69284..d8df5e87 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/MastodonPushNotification.swift @@ -11,7 +11,7 @@ public struct MastodonPushNotification: Codable { public let accessToken: String - public let notificationID: Int + public let notificationID: Int //<<< the server use `Int` type here! public let notificationType: String public let preferredLocale: String? diff --git a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift index ab6da105..c72b48a1 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/User/UserObject.swift @@ -46,6 +46,15 @@ extension UserObject { } } } + + public var notifications: Set { + switch self { + case .twitter: + return [] + case .mastodon(let object): + return object.notifications + } + } } extension UserObject { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Notification.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Notification.swift index 2a35dd67..a08dc3f6 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Notification.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Notification.swift @@ -210,5 +210,38 @@ extension APIService { return response } + + public func mastodonNotification( + notificationID: Mastodon.Entity.Notification.ID, + authenticationContext: MastodonAuthenticationContext + ) async throws -> Mastodon.Response.Content { + let response = try await Mastodon.API.Notification.notification( + session: session, + domain: authenticationContext.domain, + notificationID: notificationID, authorization: authenticationContext.authorization + ) + + let managedObjectContext = self.backgroundManagedObjectContext + try await managedObjectContext.performChanges { + guard let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user else { + assertionFailure() + return + } + _ = Persistence.MastodonNotification.createOrMerge( + in: managedObjectContext, + context: .init( + domain: authenticationContext.domain, + entity: response.value, + me: me, + notificationCache: nil, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + } + + return response + } } diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 0ad3cd31..944f1063 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -37,7 +37,7 @@ final public actor NotificationService { } public let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) public let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) - public let requestRevealNotificationPublisher = PassthroughSubject() + public let revealNotificationAction = PassthroughSubject() init( apiService: APIService, diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index ac0d3c98..3af35760 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -209,6 +209,13 @@ DB76A67127609A8700A50673 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76A67027609A8700A50673 /* RemoteProfileViewModel.swift */; }; DB77FF7F2847808C00182A0B /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DB7AB33E2744E3740035EB8A /* Floaty in Frameworks */ = {isa = PBXBuildFile; productRef = DB7AB33D2744E3740035EB8A /* Floaty */; }; + DB7FF06128853A7F00BFD55E /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF06028853A7F00BFD55E /* NotificationService.swift */; }; + DB7FF06528853A7F00BFD55E /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DB7FF05E28853A7F00BFD55E /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DB7FF06A28853AB000BFD55E /* String+Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05928853A4F00BFD55E /* String+Decode85.swift */; }; + DB7FF06B28853AB300BFD55E /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05628853A4F00BFD55E /* NotificationService+Decrypt.swift */; }; + DB7FF06C28853AB800BFD55E /* String+Escape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05528853A4F00BFD55E /* String+Escape.swift */; }; + DB7FF06E28853B1F00BFD55E /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; + DB7FF06F28853B1F00BFD55E /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB8301F7273CED0400BF5224 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F6273CED0400BF5224 /* NotificationTimelineViewController.swift */; }; DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F9273CED2E00BF5224 /* NotificationTimelineViewModel.swift */; }; DB830200273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301FF273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift */; }; @@ -319,6 +326,9 @@ DBDA8E9F24FE0FFF006750DC /* HomeTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDA8E9E24FE0FFF006750DC /* HomeTimelineViewController.swift */; }; DBDAF243274F530B00050319 /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2C8711274F4B1C00CE0398 /* SceneCoordinator.swift */; }; DBDAF244274F530B00050319 /* NeedsDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2C8712274F4B1C00CE0398 /* NeedsDependency.swift */; }; + DBE6357A28855302001C114B /* PushNotificationScratchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */; }; + DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */; }; + DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */; }; DBE71B7A26B7AF5C00DFAB8E /* StubTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */; }; DBE71B7D26B7C5FD00DFAB8E /* StubTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */; }; DBE71B7F26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */; }; @@ -338,9 +348,9 @@ DBF739CF275C247F00BF6AB5 /* DataSourceFacade+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF739CE275C247F00BF6AB5 /* DataSourceFacade+Mute.swift */; }; DBF739D1275C3EF300BF6AB5 /* DataSourceFacade+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF739D0275C3EF300BF6AB5 /* DataSourceFacade+Report.swift */; }; DBF81C7C27F6A93E00004A56 /* DataSourceFacade+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7B27F6A93E00004A56 /* DataSourceFacade+Translate.swift */; }; - DBF81C7E27F6E7F500004A56 /* AppearanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7D27F6E7F500004A56 /* AppearanceViewController.swift */; }; - DBF81C8027F6E80700004A56 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7F27F6E80700004A56 /* AppearanceView.swift */; }; - DBF81C8327F6ECF400004A56 /* AppearanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8227F6ECF400004A56 /* AppearanceViewModel.swift */; }; + DBF81C7E27F6E7F500004A56 /* AppearancePreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */; }; + DBF81C8027F6E80700004A56 /* AppearancePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */; }; + DBF81C8327F6ECF400004A56 /* AppearancePreferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */; }; DBF81C8527F709EF00004A56 /* TranslateButtonPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8427F709EF00004A56 /* TranslateButtonPreferenceView.swift */; }; DBF81C8827F7141600004A56 /* TranslationServicePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8727F7141600004A56 /* TranslationServicePreferenceView.swift */; }; DBF81C8E27F843D700004A56 /* AppIconAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF81C8C27F843D700004A56 /* AppIconAssets.swift */; }; @@ -371,6 +381,20 @@ remoteGlobalIDString = DBBBBE5D2744E8CC007ACB4B; remoteInfo = ShareExtension; }; + DB7FF06328853A7F00BFD55E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB7FF05D28853A7F00BFD55E; + remoteInfo = NotificationService; + }; + DB7FF07028853B1F00BFD55E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; + proxyType = 1; + remoteGlobalIDString = DB94B6BA26C65CB000A2E8A1; + remoteInfo = AppShared; + }; DB94B6BF26C65CB000A2E8A1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DBDA8E1624FCF8A3006750DC /* Project object */; @@ -440,6 +464,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + DB7FF07228853B1F00BFD55E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DB7FF06F28853B1F00BFD55E /* AppShared.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; DBFC0AEB276118240011E99B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -458,6 +493,7 @@ dstSubfolderSpec = 13; files = ( DB77FF7F2847808C00182A0B /* ShareExtension.appex in Embed App Extensions */, + DB7FF06528853A7F00BFD55E /* NotificationService.appex in Embed App Extensions */, DBFDCE4827F450FC00BE99E3 /* TwidereXIntent.appex in Embed App Extensions */, ); name = "Embed App Extensions"; @@ -680,6 +716,13 @@ DB76A66C2760721400A50673 /* MediaPreviewVideoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewVideoViewModel.swift; sourceTree = ""; }; DB76A66E276083CB00A50673 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = ""; }; DB76A67027609A8700A50673 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; + DB7FF05528853A4F00BFD55E /* String+Escape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Escape.swift"; sourceTree = ""; }; + DB7FF05628853A4F00BFD55E /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; + DB7FF05928853A4F00BFD55E /* String+Decode85.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Decode85.swift"; sourceTree = ""; }; + DB7FF05E28853A7F00BFD55E /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DB7FF06028853A7F00BFD55E /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + DB7FF06228853A7F00BFD55E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DB7FF06D28853AE400BFD55E /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; DB8301F6273CED0400BF5224 /* NotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewController.swift; sourceTree = ""; }; DB8301F9273CED2E00BF5224 /* NotificationTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimelineViewModel.swift; sourceTree = ""; }; DB8301FF273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTimelineViewModel+Diffable.swift"; sourceTree = ""; }; @@ -823,6 +866,9 @@ DBDA8E9724FE075E006750DC /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; DBDA8E9E24FE0FFF006750DC /* HomeTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewController.swift; sourceTree = ""; }; DBDDE72D254C084A0057CF8E /* SearchUserViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchUserViewModel+Diffable.swift"; sourceTree = ""; }; + DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewController.swift; sourceTree = ""; }; + DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewModel.swift; sourceTree = ""; }; + DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchView.swift; sourceTree = ""; }; DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewController.swift; sourceTree = ""; }; DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewModel.swift; sourceTree = ""; }; DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineCollectionViewCell.swift; sourceTree = ""; }; @@ -859,9 +905,9 @@ DBF81C7327F68AA800004A56 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; DBF81C7A27F696E000004A56 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/InfoPlist.strings; sourceTree = ""; }; DBF81C7B27F6A93E00004A56 /* DataSourceFacade+Translate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Translate.swift"; sourceTree = ""; }; - DBF81C7D27F6E7F500004A56 /* AppearanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceViewController.swift; sourceTree = ""; }; - DBF81C7F27F6E80700004A56 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; }; - DBF81C8227F6ECF400004A56 /* AppearanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceViewModel.swift; sourceTree = ""; }; + DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceViewController.swift; sourceTree = ""; }; + DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceView.swift; sourceTree = ""; }; + DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreferenceViewModel.swift; sourceTree = ""; }; DBF81C8427F709EF00004A56 /* TranslateButtonPreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateButtonPreferenceView.swift; sourceTree = ""; }; DBF81C8727F7141600004A56 /* TranslationServicePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationServicePreferenceView.swift; sourceTree = ""; }; DBF81C8C27F843D700004A56 /* AppIconAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIconAssets.swift; sourceTree = ""; }; @@ -883,6 +929,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + DB7FF05B28853A7F00BFD55E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DB7FF06E28853B1F00BFD55E /* AppShared.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB94B6B826C65CB000A2E8A1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1167,10 +1221,11 @@ children = ( DB2C8739274F4B7D00CE0398 /* List */, DB580EAE288187BD00BC4A0F /* AccountPreference */, - DBF81C8127F6E84300004A56 /* Appearance */, + DBF81C8127F6E84300004A56 /* AppearancePreference */, DB2C8733274F4B7D00CE0398 /* DisplayPreference */, - DB2C872F274F4B7D00CE0398 /* Developer */, DB2C8736274F4B7D00CE0398 /* About */, + DB2C872F274F4B7D00CE0398 /* Developer */, + DBE6357B28855546001C114B /* PushNotificationScratch */, ); path = Setting; sourceTree = ""; @@ -1595,6 +1650,19 @@ path = Video; sourceTree = ""; }; + DB7FF05F28853A7F00BFD55E /* NotificationService */ = { + isa = PBXGroup; + children = ( + DB7FF06228853A7F00BFD55E /* Info.plist */, + DB7FF06D28853AE400BFD55E /* NotificationService.entitlements */, + DB7FF06028853A7F00BFD55E /* NotificationService.swift */, + DB7FF05628853A4F00BFD55E /* NotificationService+Decrypt.swift */, + DB7FF05928853A4F00BFD55E /* String+Decode85.swift */, + DB7FF05528853A4F00BFD55E /* String+Escape.swift */, + ); + path = NotificationService; + sourceTree = ""; + }; DB8301F8273CED0700BF5224 /* NotificationTimeline */ = { isa = PBXGroup; children = ( @@ -2018,6 +2086,7 @@ DB94B6BC26C65CB000A2E8A1 /* AppShared */, DBBBBE5F2744E8CC007ACB4B /* ShareExtension */, DBFDCE4227F450FC00BE99E3 /* TwidereXIntent */, + DB7FF05F28853A7F00BFD55E /* NotificationService */, DBDA8E1F24FCF8A3006750DC /* Products */, 0ACC0E3D669CFFCB246A2D5B /* Pods */, F30A8D6C372DFF6B5AEEE327 /* Frameworks */, @@ -2033,6 +2102,7 @@ DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */, DBBBBE5E2744E8CC007ACB4B /* ShareExtension.appex */, DBFDCE4027F450FC00BE99E3 /* TwidereXIntent.appex */, + DB7FF05E28853A7F00BFD55E /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -2142,6 +2212,16 @@ path = Home; sourceTree = ""; }; + DBE6357B28855546001C114B /* PushNotificationScratch */ = { + isa = PBXGroup; + children = ( + DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */, + DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */, + DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */, + ); + path = PushNotificationScratch; + sourceTree = ""; + }; DBE71B7B26B7AF6100DFAB8E /* StubTimeline */ = { isa = PBXGroup; children = ( @@ -2288,16 +2368,16 @@ path = SearchDetail; sourceTree = ""; }; - DBF81C8127F6E84300004A56 /* Appearance */ = { + DBF81C8127F6E84300004A56 /* AppearancePreference */ = { isa = PBXGroup; children = ( DBF81C9227F8449800004A56 /* AppIcon */, DBF81C8627F709F200004A56 /* Translation */, - DBF81C7F27F6E80700004A56 /* AppearanceView.swift */, - DBF81C8227F6ECF400004A56 /* AppearanceViewModel.swift */, - DBF81C7D27F6E7F500004A56 /* AppearanceViewController.swift */, + DBF81C7D27F6E7F500004A56 /* AppearancePreferenceViewController.swift */, + DBF81C8227F6ECF400004A56 /* AppearancePreferenceViewModel.swift */, + DBF81C7F27F6E80700004A56 /* AppearancePreferenceView.swift */, ); - path = Appearance; + path = AppearancePreference; sourceTree = ""; }; DBF81C8627F709F200004A56 /* Translation */ = { @@ -2380,6 +2460,25 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + DB7FF05D28853A7F00BFD55E /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = DB7FF06628853A7F00BFD55E /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + DB7FF05A28853A7F00BFD55E /* Sources */, + DB7FF05B28853A7F00BFD55E /* Frameworks */, + DB7FF05C28853A7F00BFD55E /* Resources */, + DB7FF07228853B1F00BFD55E /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DB7FF07128853B1F00BFD55E /* PBXTargetDependency */, + ); + name = NotificationService; + productName = NotificationService; + productReference = DB7FF05E28853A7F00BFD55E /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; DB94B6BA26C65CB000A2E8A1 /* AppShared */ = { isa = PBXNativeTarget; buildConfigurationList = DB94B6C326C65CB100A2E8A1 /* Build configuration list for PBXNativeTarget "AppShared" */; @@ -2445,6 +2544,7 @@ DB94B6C026C65CB000A2E8A1 /* PBXTargetDependency */, DBFDCE4727F450FC00BE99E3 /* PBXTargetDependency */, DB77FF812847808C00182A0B /* PBXTargetDependency */, + DB7FF06428853A7F00BFD55E /* PBXTargetDependency */, ); name = TwidereX; packageProductDependencies = ( @@ -2523,10 +2623,13 @@ DBDA8E1624FCF8A3006750DC /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1340; LastUpgradeCheck = 1200; ORGANIZATIONNAME = Twidere; TargetAttributes = { + DB7FF05D28853A7F00BFD55E = { + CreatedOnToolsVersion = 13.4.1; + }; DB94B6BA26C65CB000A2E8A1 = { CreatedOnToolsVersion = 13.0; LastSwiftMigration = 1300; @@ -2587,11 +2690,19 @@ DB94B6BA26C65CB000A2E8A1 /* AppShared */, DBBBBE5D2744E8CC007ACB4B /* ShareExtension */, DBFDCE3F27F450FC00BE99E3 /* TwidereXIntent */, + DB7FF05D28853A7F00BFD55E /* NotificationService */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + DB7FF05C28853A7F00BFD55E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB94B6B926C65CB000A2E8A1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2841,6 +2952,17 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + DB7FF05A28853A7F00BFD55E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DB7FF06128853A7F00BFD55E /* NotificationService.swift in Sources */, + DB7FF06C28853AB800BFD55E /* String+Escape.swift in Sources */, + DB7FF06B28853AB300BFD55E /* NotificationService+Decrypt.swift in Sources */, + DB7FF06A28853AB000BFD55E /* String+Decode85.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DB94B6B726C65CB000A2E8A1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2990,7 +3112,7 @@ DB580EB9288187BD00BC4A0F /* AccountPreferenceViewModel.swift in Sources */, DB33A4A925A319A0003CED7D /* ActionToolbarContainer.swift in Sources */, DB2C873C274F4B7D00CE0398 /* DeveloperView.swift in Sources */, - DBF81C8327F6ECF400004A56 /* AppearanceViewModel.swift in Sources */, + DBF81C8327F6ECF400004A56 /* AppearancePreferenceViewModel.swift in Sources */, DB2C8742274F4B7D00CE0398 /* AboutViewController.swift in Sources */, DB442465285AD8660095AECF /* ListStatusTimelineViewModel+Diffable.swift in Sources */, DB5FD9B326D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift in Sources */, @@ -3049,6 +3171,7 @@ DB25C4C8277994B800EC1435 /* CenterFootnoteLabelTableViewCell.swift in Sources */, DBA210352759D7B1000B7CB2 /* FollowingListViewModel+Diffable.swift in Sources */, DB2C8744274F4B7D00CE0398 /* SettingListViewController.swift in Sources */, + DBE6357F288555AE001C114B /* PushNotificationScratchView.swift in Sources */, DB47AB3E27CE1F9E00CD73C7 /* TimelineTopLoaderTableViewCell.swift in Sources */, DB0AD4E62858742D0002ABDB /* UserMediaTimelineViewModel+Diffable.swift in Sources */, DB02C77027350D8A007EA0BF /* SearchHashtagViewModel.swift in Sources */, @@ -3083,7 +3206,7 @@ DB5632B026DCED1300FC893F /* StatusThreadRootTableViewCell.swift in Sources */, DB8761C4274552FB00BA7EE2 /* HashtagData.swift in Sources */, DBDA8E2224FCF8A3006750DC /* AppDelegate.swift in Sources */, - DBF81C7E27F6E7F500004A56 /* AppearanceViewController.swift in Sources */, + DBF81C7E27F6E7F500004A56 /* AppearancePreferenceViewController.swift in Sources */, DB46D11F27DB2B50003B8BA1 /* ListUserViewModel+Diffable.swift in Sources */, DB442459285A42B50095AECF /* HashtagTimelineViewController.swift in Sources */, DB56329726DCBE1600FC893F /* StatusThreadViewController.swift in Sources */, @@ -3101,6 +3224,7 @@ DBDAF244274F530B00050319 /* NeedsDependency.swift in Sources */, DB8761C3274552FB00BA7EE2 /* HashtagSection.swift in Sources */, DB9B325C2857493D00AC818D /* UserTimelineViewModel+Diffable.swift in Sources */, + DBE6357A28855302001C114B /* PushNotificationScratchViewController.swift in Sources */, DBF81C8E27F843D700004A56 /* AppIconAssets.swift in Sources */, DB262A3B272288FC00D18EF3 /* SearchViewController.swift in Sources */, DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */, @@ -3116,6 +3240,7 @@ DBB04A372861AF2D003799CA /* TrendPlaceView.swift in Sources */, DB6BCD6E277ADC0900847054 /* TrendViewModel.swift in Sources */, DB51DC4E27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift in Sources */, + DBE6357D2885557C001C114B /* PushNotificationScratchViewModel.swift in Sources */, DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */, DB6DF3E0252060AA00E8A273 /* ProfileViewModel.swift in Sources */, DB262A332721377800D18EF3 /* DataSourceFacade+Block.swift in Sources */, @@ -3158,7 +3283,7 @@ DBB04A352861AE4D003799CA /* TrendPlaceViewModel.swift in Sources */, DB2C8743274F4B7D00CE0398 /* SettingListView.swift in Sources */, DB76A650275F1C3200A50673 /* MediaPreviewTransitionController.swift in Sources */, - DBF81C8027F6E80700004A56 /* AppearanceView.swift in Sources */, + DBF81C8027F6E80700004A56 /* AppearancePreferenceView.swift in Sources */, DB8761C5274552FB00BA7EE2 /* HashtagItem.swift in Sources */, DB76A65B275F49AE00A50673 /* MediaInfoDescriptionView.swift in Sources */, DB76A64D275DFA0600A50673 /* TwitterStatusThreadReplyViewModel+State.swift in Sources */, @@ -3214,6 +3339,16 @@ target = DBBBBE5D2744E8CC007ACB4B /* ShareExtension */; targetProxy = DB77FF802847808C00182A0B /* PBXContainerItemProxy */; }; + DB7FF06428853A7F00BFD55E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB7FF05D28853A7F00BFD55E /* NotificationService */; + targetProxy = DB7FF06328853A7F00BFD55E /* PBXContainerItemProxy */; + }; + DB7FF07128853B1F00BFD55E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; + targetProxy = DB7FF07028853B1F00BFD55E /* PBXContainerItemProxy */; + }; DB94B6C026C65CB000A2E8A1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DB94B6BA26C65CB000A2E8A1 /* AppShared */; @@ -3552,7 +3687,6 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3565,9 +3699,98 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; + DB7FF06728853A7F00BFD55E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2.5; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + DB7FF06828853A7F00BFD55E /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2.5; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + DB7FF06928853A7F00BFD55E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 111; + DEVELOPMENT_TEAM = 7LFDZ96332; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NotificationService/Info.plist; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.2.5; + PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; DB94B6C426C65CB100A2E8A1 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5635061D4E24E9A2C69E90ED /* Pods-AppShared.debug.xcconfig */; @@ -3977,7 +4200,6 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3990,6 +4212,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -4005,7 +4228,6 @@ INFOPLIST_FILE = TwidereXIntent/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TwidereXIntent; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Twidere. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4018,12 +4240,23 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + DB7FF06628853A7F00BFD55E /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DB7FF06728853A7F00BFD55E /* Debug */, + DB7FF06828853A7F00BFD55E /* Profile */, + DB7FF06928853A7F00BFD55E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DB94B6C326C65CB100A2E8A1 /* Build configuration list for PBXNativeTarget "AppShared" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index cfb00a41..87ff25d6 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -6,13 +6,19 @@ // Copyright © 2020 Dimension. All rights reserved. // +import os.log import UIKit +import Combine import SafariServices import CoreDataStack import TwidereUI final public class SceneCoordinator { + let logger = Logger(subsystem: "SceneCoordinator", category: "Coordinator") + + private var disposeBag = Set() + private weak var scene: UIScene! private weak var sceneDelegate: SceneDelegate! private weak var context: AppContext! @@ -26,8 +32,10 @@ final public class SceneCoordinator { self.scene = scene self.sceneDelegate = sceneDelegate self.context = context + // end init scene.session.sceneCoordinator = self + setupPushNotificationRevealActionObserver() } } @@ -88,13 +96,15 @@ extension SceneCoordinator { // Settings case setting(viewModel: SettingListViewModel) case accountPreference(viewModel: AccountPreferenceViewModel) - case appearance + case appearancePreference case displayPreference case about #if DEBUG case developer case stubTimeline + + case pushNotificationScratch #endif case safari(url: String) @@ -220,6 +230,17 @@ extension SceneCoordinator { return viewController } + + func switchToTabBar(tab: TabBarItem) { + // root is MainTabBarController + if let mainTabBarController = sceneDelegate.window?.rootViewController as? MainTabBarController { + mainTabBarController.select(tab: tab, isMainTabBarControllerActive: true) + } + // root is ContentSplitViewController + if let contentSplitViewController = sceneDelegate.window?.rootViewController as? ContentSplitViewController { + contentSplitViewController.select(tab: tab) + } + } } @@ -342,8 +363,8 @@ private extension SceneCoordinator { let _viewController = AccountPreferenceViewController() _viewController.viewModel = viewModel viewController = _viewController - case .appearance: - viewController = AppearanceViewController() + case .appearancePreference: + viewController = AppearancePreferenceViewController() case .displayPreference: viewController = DisplayPreferenceViewController() case .about: @@ -353,6 +374,8 @@ private extension SceneCoordinator { viewController = DeveloperViewController() case .stubTimeline: viewController = StubTimelineViewController() + case .pushNotificationScratch: + viewController = PushNotificationScratchViewController() #endif case .safari(let url): // escape non-ascii characters @@ -384,3 +407,118 @@ private extension SceneCoordinator { } } + +extension SceneCoordinator { + + private func setupPushNotificationRevealActionObserver() { + Task { + await context.notificationService.revealNotificationAction + .receive(on: DispatchQueue.main) + .sink { [weak self] pushNotification in + guard let self = self else { return } + Task { + await self.coordinate(pushNotification: pushNotification) + } + } + .store(in: &self.disposeBag) + } // end Task + } + + // TODO: Twitter push notification + @MainActor + private func coordinate(pushNotification: MastodonPushNotification) async { + do { + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: pushNotification.accessToken) + request.fetchLimit = 1 + guard let authentication = try context.managedObjectContext.fetch(request).first else { + assertionFailure() + return + } + + // 1. active notification account + guard let currentAuthenticationContext = context.authenticationService.activeAuthenticationContext else { + // discard task if no available account + return + } + let needsSwitchActiveAccount: Bool = { + switch currentAuthenticationContext { + case .mastodon(let authenticationContext): + let result = authenticationContext.authorization.accessToken != pushNotification.accessToken + return result + case .twitter: + return true + } + }() + if needsSwitchActiveAccount { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] needsSwitchActiveAccount: \(needsSwitchActiveAccount)") + let isActive = try await context.authenticationService.activeAuthenticationIndex(record: authentication.authenticationIndex.asRecrod) + guard isActive else { return } + } + + // 2. open notification tab + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] switch to notification tab") + switchToTabBar(tab: .notification) + + // 3. load notification + let authenticationContext = MastodonAuthenticationContext(authentication: authentication) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] fetch notification: \(pushNotification.notificationID)…") + let response = try await context.apiService.mastodonNotification( + notificationID: String(pushNotification.notificationID), + authenticationContext: authenticationContext + ) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] fetch notification success") + + // 4. push to notification content + let from = sceneDelegate.window?.rootViewController?.topMost + + let notification = response.value + switch notification.type { + case .follow: + let remoteProfileViewModel = RemoteProfileViewModel( + context: context, + profileContext: .mastodon(.userID(notification.account.id)) + ) + present( + scene: .profile(viewModel: remoteProfileViewModel), + from: from, + transition: .show + ) + case .followRequest: + // do nothing + break + case .mention, .reblog, .favourite, .poll, .status: + // FIXME: add RemoteViewModel + guard let status = notification.status else { + return + } + let request = MastodonStatus.sortedFetchRequest + request.predicate = MastodonStatus.predicate( + domain: authenticationContext.domain, + id: status.id + ) + request.fetchLimit = 1 + guard let root = try context.managedObjectContext.fetch(request).first else { + return + } + let statusThreadViewModel = StatusThreadViewModel( + context: context, + root: .root(context: .init(status: .mastodon(record: root.asRecrod))) + ) + present( + scene: .statusThread(viewModel: statusThreadViewModel), + from: from, + transition: .show + ) + case ._other: + assertionFailure() + break + } + + } catch { + assertionFailure(error.localizedDescription) + return + } + } + +} diff --git a/TwidereX/Scene/Root/ContentSplitViewController.swift b/TwidereX/Scene/Root/ContentSplitViewController.swift index 3c05458f..cd06d6f5 100644 --- a/TwidereX/Scene/Root/ContentSplitViewController.swift +++ b/TwidereX/Scene/Root/ContentSplitViewController.swift @@ -127,6 +127,12 @@ extension ContentSplitViewController { } +extension ContentSplitViewController { + func select(tab: TabBarItem) { + sidebarViewController.viewModel.setActiveTab(item: tab) + } +} + extension ContentSplitViewController { private func updateConstraint(_ previousTraitCollection: UITraitCollection?) { diff --git a/TwidereX/Scene/Setting/Appearance/AppIconPreferenceView.swift b/TwidereX/Scene/Setting/AppearancePreference/AppIconPreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/Appearance/AppIconPreferenceView.swift rename to TwidereX/Scene/Setting/AppearancePreference/AppIconPreferenceView.swift diff --git a/TwidereX/Scene/Setting/Appearance/AppearanceView.swift b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift similarity index 92% rename from TwidereX/Scene/Setting/Appearance/AppearanceView.swift rename to TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift index 6de2c607..195cedcc 100644 --- a/TwidereX/Scene/Setting/Appearance/AppearanceView.swift +++ b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceView.swift @@ -10,9 +10,9 @@ import SwiftUI import TwidereLocalization import TwidereUI -struct AppearanceView: View { +struct AppearancePreferenceView: View { - @ObservedObject var viewModel: AppearanceViewModel + @ObservedObject var viewModel: AppearancePreferenceViewModel @State private var isTranslateButtonPreferenceSheetPresented = false @@ -74,7 +74,7 @@ struct AppearanceView: View { struct AppearanceView_Previews: PreviewProvider { static var previews: some View { NavigationView { - AppearanceView(viewModel: AppearanceViewModel(context: .shared)) + AppearancePreferenceView(viewModel: AppearancePreferenceViewModel(context: .shared)) .navigationBarTitle(Text(L10n.Scene.Settings.Appearance.title)) .navigationBarTitleDisplayMode(.inline) } diff --git a/TwidereX/Scene/Setting/Appearance/AppearanceViewController.swift b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift similarity index 75% rename from TwidereX/Scene/Setting/Appearance/AppearanceViewController.swift rename to TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift index e448702d..3d756d40 100644 --- a/TwidereX/Scene/Setting/Appearance/AppearanceViewController.swift +++ b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewController.swift @@ -1,5 +1,5 @@ // -// AppearanceViewController.swift +// AppearancePreferenceViewController.swift // TwidereX // // Created by MainasuK on 2022-4-1. @@ -12,19 +12,16 @@ import Combine import SwiftUI import TwidereLocalization -final class AppearanceViewController: UIViewController, NeedsDependency { +final class AppearancePreferenceViewController: UIViewController, NeedsDependency { - let logger = Logger(subsystem: "AppearanceViewController", category: "ViewController") + let logger = Logger(subsystem: "AppearancePreferenceViewController", category: "ViewController") weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() - private(set) lazy var viewModel = AppearanceViewModel(context: context) - - // weak var delegate: EditListViewControllerDelegate? - - private(set) lazy var appearanceView = AppearanceView(viewModel: viewModel) + private(set) lazy var viewModel = AppearancePreferenceViewModel(context: context) + private(set) lazy var appearanceView = AppearancePreferenceView(viewModel: viewModel) deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -32,7 +29,7 @@ final class AppearanceViewController: UIViewController, NeedsDependency { } -extension AppearanceViewController { +extension AppearancePreferenceViewController { override func viewDidLoad() { super.viewDidLoad() diff --git a/TwidereX/Scene/Setting/Appearance/AppearanceViewModel.swift b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift similarity index 92% rename from TwidereX/Scene/Setting/Appearance/AppearanceViewModel.swift rename to TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift index b148becd..01f828a2 100644 --- a/TwidereX/Scene/Setting/Appearance/AppearanceViewModel.swift +++ b/TwidereX/Scene/Setting/AppearancePreference/AppearancePreferenceViewModel.swift @@ -1,5 +1,5 @@ // -// AppearanceViewModel.swift +// AppearancePreferenceViewModel.swift // TwidereX // // Created by MainasuK on 2022-4-1. @@ -16,7 +16,7 @@ import TwidereCore import TwitterSDK import MastodonSDK -final class AppearanceViewModel: ObservableObject { +final class AppearancePreferenceViewModel: ObservableObject { // input let context: AppContext diff --git a/TwidereX/Scene/Setting/Appearance/TranslateButtonPreferenceView.swift b/TwidereX/Scene/Setting/AppearancePreference/TranslateButtonPreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/Appearance/TranslateButtonPreferenceView.swift rename to TwidereX/Scene/Setting/AppearancePreference/TranslateButtonPreferenceView.swift diff --git a/TwidereX/Scene/Setting/Appearance/TranslationServicePreferenceView.swift b/TwidereX/Scene/Setting/AppearancePreference/TranslationServicePreferenceView.swift similarity index 100% rename from TwidereX/Scene/Setting/Appearance/TranslationServicePreferenceView.swift rename to TwidereX/Scene/Setting/AppearancePreference/TranslationServicePreferenceView.swift diff --git a/TwidereX/Scene/Setting/List/SettingListViewController.swift b/TwidereX/Scene/Setting/List/SettingListViewController.swift index 10eb07b3..d2c59489 100644 --- a/TwidereX/Scene/Setting/List/SettingListViewController.swift +++ b/TwidereX/Scene/Setting/List/SettingListViewController.swift @@ -64,7 +64,7 @@ extension SettingListViewController { transition: .show ) case .appearance: - self.coordinator.present(scene: .appearance, from: self, transition: .show) + self.coordinator.present(scene: .appearancePreference, from: self, transition: .show) case .display: self.coordinator.present(scene: .displayPreference, from: self, transition: .show) case .layout: diff --git a/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchView.swift b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchView.swift new file mode 100644 index 00000000..32f998bc --- /dev/null +++ b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchView.swift @@ -0,0 +1,38 @@ +// +// PushNotificationScratchView.swift +// TwidereX +// +// Created by MainasuK on 2022-7-18. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import SwiftUI + +struct PushNotificationScratchView: View { + + @ObservedObject var viewModel: PushNotificationScratchViewModel + + var body: some View { + Form { + Section { + Toggle(isOn: $viewModel.isRandomNotification) { + Text("Random Notification") + } + if !viewModel.isRandomNotification { + TextField("Notification ID", text: $viewModel.notificationID) + } + } + Section { + Picker("Account:", selection: $viewModel.activeAccountIndex) { + ForEach(Array(viewModel.accounts.enumerated()), id: \.0) { index, account in + let username = "@" + account.username + Text(username) + } + } + .pickerStyle(.inline) + } + } // end Form + } // end body + +} diff --git a/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewController.swift b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewController.swift new file mode 100644 index 00000000..e02c56b5 --- /dev/null +++ b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewController.swift @@ -0,0 +1,104 @@ +// +// PushNotificationScratchViewController.swift +// TwidereX +// +// Created by MainasuK on 2022-7-18. +// Copyright © 2022 Twidere. All rights reserved. +// + +import os.log +import UIKit +import SwiftUI +import TwidereCore + +final class PushNotificationScratchViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + private(set) lazy var viewModel = PushNotificationScratchViewModel(context: context) + private(set) lazy var accountPreferenceView = PushNotificationScratchView(viewModel: viewModel) + +} + +extension PushNotificationScratchViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Push Notification Scratch" + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(PushNotificationScratchViewController.cancelBarButtonItemDidPressed(_:))) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show", style: .plain, target: self, action: #selector(PushNotificationScratchViewController.showBarButtonItemDidPressed(_:))) + + let hostingController = UIHostingController(rootView: accountPreferenceView) + addChild(hostingController) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingController.view) + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + hostingController.didMove(toParent: self) + } + +} + +extension PushNotificationScratchViewController { + + @objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) { + dismiss(animated: true) + } + + @objc private func showBarButtonItemDidPressed(_ sender: UIBarButtonItem) { + Task { + // accessToken + guard let account = viewModel.accounts[safe: viewModel.activeAccountIndex], + let authenticationContext = account.authenticationContext + else { + return + } + + let _accessToken: String? = { + switch authenticationContext { + case .twitter: + return nil + case .mastodon(let authenticationContext): + return authenticationContext.authorization.accessToken + } + }() + guard let accessToken = _accessToken else { + return + } + + // notification ID + let notificationID: String = { + if viewModel.isRandomNotification { + return account.notifications.randomElement()?.id ?? "" + } else { + return viewModel.notificationID + } + }() + + let pushNotification = MastodonPushNotification( + accessToken: accessToken, + notificationID: Int(notificationID) ?? -1, + notificationType: "", + preferredLocale: nil, + icon: nil, + title: "", + body: "" + ) + + self.dismiss(animated: true) { + Task { + await self.context.notificationService.revealNotificationAction.send(pushNotification) + } // end Task + } + } // end Task + } + +} + + diff --git a/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift new file mode 100644 index 00000000..86891121 --- /dev/null +++ b/TwidereX/Scene/Setting/PushNotificationScratch/PushNotificationScratchViewModel.swift @@ -0,0 +1,34 @@ +// +// PushNotificationScratchViewModel.swift +// TwidereX +// +// Created by MainasuK on 2022-7-18. +// Copyright © 2022 Twidere. All rights reserved. +// + +import Foundation +import CoreData +import CoreDataStack +import TwidereCore + +final class PushNotificationScratchViewModel: ObservableObject { + + // input + let context: AppContext + + @Published var isRandomNotification = true + @Published var notificationID = "" + + @Published var accounts: [UserObject] + @Published var activeAccountIndex: Int = 0 + + // output + + init(context: AppContext) { + self.context = context + // end init + + accounts = context.authenticationService.authenticationIndexes.compactMap { $0.user } + } + +} diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index eca0de80..d4a09fce 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -92,6 +92,10 @@ extension HomeTimelineViewController { transition: .alertController(animated: true, completion: nil) ) }), + UIAction(title: "Push Notification", attributes: [], state: .off, handler: { [weak self] action in + guard let self = self else { return } + self.showPushNotification() + }), UIAction(title: "Account List", image: nil, attributes: [], handler: { [weak self] action in guard let self = self else { return } self.showAccountListAction(action) @@ -300,6 +304,14 @@ extension HomeTimelineViewController { } // end Task } + @objc private func showPushNotification() { + coordinator.present( + scene: .pushNotificationScratch, + from: nil, + transition: .modal(animated: true) + ) + } + @objc private func showStubTimelineAction(_ sender: UIAction) { coordinator.present(scene: .stubTimeline, from: self, transition: .show) } diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index f7a4f7ee..7ddc5605 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -103,14 +103,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate { willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push]", ((#file as NSString).lastPathComponent), #line, #function) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH]") guard let pushNotification = AppDelegate.mastodonPushNotification(from: notification) else { completionHandler([]) return } let notificationID = String(pushNotification.notificationID) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH] notification \(notificationID)") let accessToken = pushNotification.accessToken UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) @@ -122,6 +122,28 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound]) } + // response to user action for notification (e.g. redirect to post) + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [PUSH]") + + guard let pushNotification = AppDelegate.mastodonPushNotification(from: response.notification) else { + completionHandler() + return + } + + let notificationID = String(pushNotification.notificationID) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + Task { + await appContext.notificationService.receive(pushNotification: pushNotification) + await appContext.notificationService.revealNotificationAction.send(pushNotification) + completionHandler() + } // end Task + } + private static func mastodonPushNotification(from notification: UNNotification) -> MastodonPushNotification? { guard let plaintext = notification.request.content.userInfo["plaintext"] as? Data, let mastodonPushNotification = try? JSONDecoder().decode(MastodonPushNotification.self, from: plaintext) else { From bfb85db9be7f4594f7cb1c8f2f326d12f5c39a1c Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 19 Jul 2022 16:47:01 +0800 Subject: [PATCH 22/38] feat: add restore scroll position supports for status list timeline --- TwidereX.xcodeproj/project.pbxproj | 21 +++- .../List/ListTimelineViewController.swift | 97 +++++++++++++++++++ .../Base/List/ListTimelineViewModel.swift | 11 +++ .../Home/HomeTimelineViewController.swift | 24 +++++ .../Timeline/Home/HomeTimelineViewModel.swift | 1 + .../List/ListStatusTimelineViewModel.swift | 2 +- .../TwidereX-Bridging-Header.h | 4 + .../Notification+Name+HandleTapAction.swift | 12 +++ .../UIStatusBarManager+HandleTapAction.m | 46 +++++++++ 9 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 TwidereX/Supporting Files/TwidereX-Bridging-Header.h create mode 100644 TwidereX/Vender/Notification+Name+HandleTapAction.swift create mode 100644 TwidereX/Vender/UIStatusBarManager+HandleTapAction.m diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 3af35760..1448d2a7 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -127,6 +127,8 @@ DB51DC432718117900A0D8FB /* StatusMediaGalleryCollectionCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC422718117900A0D8FB /* StatusMediaGalleryCollectionCell+ViewModel.swift */; }; DB51DC4E27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC4D27181D7500A0D8FB /* CoverFlowStackMediaCollectionCell.swift */; }; DB51DC5027181DE500A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51DC4F27181DE400A0D8FB /* CoverFlowStackMediaCollectionCell+ViewModel.swift */; }; + DB522F1628869DAE0088017C /* Notification+Name+HandleTapAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */; }; + DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */ = {isa = PBXBuildFile; fileRef = DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */; }; DB56329726DCBE1600FC893F /* StatusThreadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329626DCBE1600FC893F /* StatusThreadViewController.swift */; }; DB56329A26DCBE7300FC893F /* StatusThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329926DCBE7300FC893F /* StatusThreadViewModel.swift */; }; DB56329C26DCC23700FC893F /* DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB56329B26DCC23700FC893F /* DataSourceProvider.swift */; }; @@ -869,6 +871,9 @@ DBE6357928855302001C114B /* PushNotificationScratchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewController.swift; sourceTree = ""; }; DBE6357C2885557C001C114B /* PushNotificationScratchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchViewModel.swift; sourceTree = ""; }; DBE6357E288555AE001C114B /* PushNotificationScratchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationScratchView.swift; sourceTree = ""; }; + DBE635822886940A001C114B /* TwidereX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TwidereX-Bridging-Header.h"; sourceTree = ""; }; + DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIStatusBarManager+HandleTapAction.m"; sourceTree = ""; }; + DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name+HandleTapAction.swift"; sourceTree = ""; }; DBE71B7926B7AF5C00DFAB8E /* StubTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewController.swift; sourceTree = ""; }; DBE71B7C26B7C5FD00DFAB8E /* StubTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineViewModel.swift; sourceTree = ""; }; DBE71B7E26B7D68500DFAB8E /* StubTimelineCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubTimelineCollectionViewCell.swift; sourceTree = ""; }; @@ -1761,6 +1766,8 @@ children = ( DBA5FA182553DCBC00D2E98E /* TransitioningMath.swift */, DB42411526C3EB9100B6C5F8 /* ReadabilityPadding.swift */, + DBE6358528869441001C114B /* Notification+Name+HandleTapAction.swift */, + DBE635832886940B001C114B /* UIStatusBarManager+HandleTapAction.m */, ); path = Vender; sourceTree = ""; @@ -2152,6 +2159,7 @@ DBDA8E5324FDF3D6006750DC /* Supporting Files */ = { isa = PBXGroup; children = ( + DBE635822886940A001C114B /* TwidereX-Bridging-Header.h */, DBDA8E2124FCF8A3006750DC /* AppDelegate.swift */, DBDA8E2324FCF8A3006750DC /* SceneDelegate.swift */, DBDA8E2724FCF8A3006750DC /* Main.storyboard */, @@ -2639,6 +2647,7 @@ }; DBDA8E1D24FCF8A3006750DC = { CreatedOnToolsVersion = 12.0; + LastSwiftMigration = 1400; }; DBDA8E3324FCF8A7006750DC = { CreatedOnToolsVersion = 12.0; @@ -3088,6 +3097,7 @@ DBA5FA192553DCBC00D2E98E /* TransitioningMath.swift in Sources */, DB56329C26DCC23700FC893F /* DataSourceProvider.swift in Sources */, DB44A56226C4FEAB004C8B78 /* WelcomeViewModel.swift in Sources */, + DB522F172886A08F0088017C /* UIStatusBarManager+HandleTapAction.m in Sources */, DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, DBAA898E2758CF01001C273B /* DrawerSidebarHeaderView+ViewModel.swift in Sources */, DBADCDC82826658700D1CA4E /* MediaPreviewableViewController.swift in Sources */, @@ -3100,6 +3110,7 @@ DB0CC4BA27D5F7BA00A051B4 /* CompositeListViewModel+Diffable.swift in Sources */, DB02C77527351DA2007EA0BF /* HashtagTableViewCell+ViewModel.swift in Sources */, DB0AD4E22858734A0002ABDB /* UserMediaTimelineViewModel.swift in Sources */, + DB522F1628869DAE0088017C /* Notification+Name+HandleTapAction.swift in Sources */, DB86433C26E898C5000C9879 /* DataSourceFacade+Like.swift in Sources */, DB6BCD70277AEAC700847054 /* TrendTableViewCell.swift in Sources */, DB442461285AD8530095AECF /* ListStatusTimelineViewController.swift in Sources */, @@ -3541,10 +3552,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9510579CE5350F61425470C1 /* Pods-TwidereX.profile.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = ""; ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -3560,6 +3571,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -4038,10 +4050,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = F58BC0DB264850C274FDE699 /* Pods-TwidereX.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = ""; ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -4057,6 +4069,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -4067,10 +4081,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 4423B1DC6D103908A7B752AF /* Pods-TwidereX.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = ""; ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -4086,6 +4100,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "TwidereX/Supporting Files/TwidereX-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift index c57c843d..cec59007 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewController.swift @@ -76,6 +76,30 @@ extension ListTimelineViewController { self.viewModel.stateMachine.enter(TimelineViewModel.LoadOldestState.Loading.self) } .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: .statusBarTapped, object: nil) + .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: false) + .sink { [weak self] notification in + guard let self = self else { return } + guard let _ = self.view.window else { return } // displaying + + // https://developer.limneos.net/index.php?ios=13.1.3&framework=UIKitCore.framework&header=UIStatusBarTapAction.h + guard let action = notification.object as AnyObject?, + let xPosition = action.value(forKey: "xPosition") as? Double + else { return } + + let viewFrameInWindow = self.view.convert(self.view.frame, to: nil) + guard xPosition >= viewFrameInWindow.minX && xPosition <= viewFrameInWindow.maxX else { return } + + // works on iOS 14, 15 + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): receive notification \(xPosition)") + + // check if scroll to top + guard self.shouldRestoreScrollPosition() else { return } + self.restorePositionWhenScrollToTop() + } + .store(in: &disposeBag) } override func viewWillAppear(_ animated: Bool) { @@ -158,6 +182,78 @@ extension ListTimelineViewController: CellFrameCacheContainer { } } + +// MARK: - UIScrollViewDelegate +extension ListTimelineViewController { + + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + switch scrollView { + case tableView: + + let indexPath = IndexPath(row: 0, section: 0) + guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { + return true + } + // save position + savePositionBeforeScrollToTop() + // override by custom scrollToRow + tableView.scrollToRow(at: indexPath, at: .top, animated: true) + return false + default: + assertionFailure() + return true + } + } + + @objc func savePositionBeforeScrollToTop() { + // check save action interval + // should not fast than 0.5s to prevent save when scrollToTop on-flying + if let record = viewModel.scrollPositionRecord { + let now = Date() + guard now.timeIntervalSince(record.timestamp) > 0.5 else { + // skip this save action + return + } + } + + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let anchorIndexPaths = tableView.indexPathsForVisibleRows?.sorted() else { return } + guard !anchorIndexPaths.isEmpty else { return } + let anchorIndexPath = anchorIndexPaths[anchorIndexPaths.count / 2] + guard let anchorItem = diffableDataSource.itemIdentifier(for: anchorIndexPath) else { return } + + let offset: CGFloat = { + guard let anchorCell = tableView.cellForRow(at: anchorIndexPath) else { return 0 } + let cellFrameInView = tableView.convert(anchorCell.frame, to: view) + return cellFrameInView.origin.y + }() + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): save position record for \(anchorIndexPath) with offset: \(offset)") + viewModel.scrollPositionRecord = HomeTimelineViewModel.ScrollPositionRecord( + item: anchorItem, + offset: offset, + timestamp: Date() + ) + } + + private func shouldRestoreScrollPosition() -> Bool { + // check if scroll to top + guard self.tableView.safeAreaInsets.top > 0 else { return false } + let zeroOffset = -self.tableView.safeAreaInsets.top + return abs(self.tableView.contentOffset.y - zeroOffset) < 2.0 + } + + @objc func restorePositionWhenScrollToTop() { + guard let diffableDataSource = self.viewModel.diffableDataSource else { return } + guard let record = self.viewModel.scrollPositionRecord, + let indexPath = diffableDataSource.indexPath(for: record.item) + else { return } + + tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + viewModel.scrollPositionRecord = nil + } + +} + // MARK: - UITableViewDelegate extension ListTimelineViewController: UITableViewDelegate, AutoGenerateTableViewDelegate { // sourcery:inline:ListTimelineView @@ -220,6 +316,7 @@ extension ListTimelineViewController: ScrollViewContainer { } else { let indexPath = IndexPath(row: 0, section: 0) guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return } + savePositionBeforeScrollToTop() tableView.scrollToRow(at: indexPath, at: .top, animated: true) } } diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index 61744e4b..62261dde 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -10,6 +10,9 @@ import UIKit class ListTimelineViewModel: TimelineViewModel { + // input + @Published var scrollPositionRecord: ScrollPositionRecord? = nil + var diffableDataSource: UITableViewDiffableDataSource? @MainActor @@ -100,3 +103,11 @@ extension ListTimelineViewModel { } } + +extension ListTimelineViewModel { + struct ScrollPositionRecord { + let item: StatusItem + let offset: CGFloat + let timestamp: Date + } +} diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift index 138dda9d..52e0749d 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController.swift @@ -207,6 +207,30 @@ extension HomeTimelineViewController { } } + override func savePositionBeforeScrollToTop() { + guard let viewModel = self.viewModel as? HomeTimelineViewModel else { return } + let latestUnreadStatusItem = viewModel.latestUnreadStatusItem + viewModel.latestUnreadStatusItemBeforeScrollToTop = latestUnreadStatusItem + + super.savePositionBeforeScrollToTop() + } + + override func restorePositionWhenScrollToTop() { + super.restorePositionWhenScrollToTop() + + guard let viewModel = self.viewModel as? HomeTimelineViewModel else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + viewModel.latestUnreadStatusItem = viewModel.latestUnreadStatusItemBeforeScrollToTop + viewModel.latestUnreadStatusItemBeforeScrollToTop = nil + + if let latestUnreadStatusItem = viewModel.latestUnreadStatusItem, + let index = viewModel.diffableDataSource?.indexPath(for: latestUnreadStatusItem) + { + viewModel.unreadItemCount = index.row // trigger update + } + } + } + } // MARK: - UITableViewDelegate diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift index baad18e2..77b47622 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift @@ -14,6 +14,7 @@ final class HomeTimelineViewModel: ListTimelineViewModel { // input var isUpdaingDataSource = false var latestUnreadStatusItem: StatusItem? + var latestUnreadStatusItemBeforeScrollToTop: StatusItem? // output @Published var unreadItemCount = 0 diff --git a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift index 5acd3ff8..164340b0 100644 --- a/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/List/ListStatusTimelineViewModel.swift @@ -13,7 +13,7 @@ import CoreDataStack import TwidereCore final class ListStatusTimelineViewModel: ListTimelineViewModel { - + // output @Published var title: String? @Published var isDeleted = false diff --git a/TwidereX/Supporting Files/TwidereX-Bridging-Header.h b/TwidereX/Supporting Files/TwidereX-Bridging-Header.h new file mode 100644 index 00000000..1b2cb5d6 --- /dev/null +++ b/TwidereX/Supporting Files/TwidereX-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/TwidereX/Vender/Notification+Name+HandleTapAction.swift b/TwidereX/Vender/Notification+Name+HandleTapAction.swift new file mode 100644 index 00000000..00a21a46 --- /dev/null +++ b/TwidereX/Vender/Notification+Name+HandleTapAction.swift @@ -0,0 +1,12 @@ +// +// Notification+Name+HandleTapAction.swift +// +// +// Created by MainasuK on 2022-7-19. +// + +import Foundation + +extension Notification.Name { + public static let statusBarTapped = Notification.Name(rawValue: "com.twidere.TwidereX.statusBarTapped") +} diff --git a/TwidereX/Vender/UIStatusBarManager+HandleTapAction.m b/TwidereX/Vender/UIStatusBarManager+HandleTapAction.m new file mode 100644 index 00000000..01022c5e --- /dev/null +++ b/TwidereX/Vender/UIStatusBarManager+HandleTapAction.m @@ -0,0 +1,46 @@ +// +// UIStatusBarManager+HandleTapAction.m +// TwidereX +// +// Created by MainasuK on 2022-7-19. +// Copyright © 2022 Twidere. All rights reserved. +// + +#import +#import +#import +#import + +@implementation UIStatusBarManager (CAPHandleTapAction) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class class = [self class]; + SEL originalSelector = NSSelectorFromString(@"handleTapAction:"); + SEL swizzledSelector = @selector(custom_handleTapAction:); + + Method originalMethod = class_getInstanceMethod(self, originalSelector); + Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); + + BOOL didAddMethod = class_addMethod(class, + originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)); + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzledMethod); + } + }); +} + +-(void)custom_handleTapAction:(id)sender { + [[NSNotificationCenter defaultCenter] postNotificationName:@"com.twidere.TwidereX.statusBarTapped" object:sender]; + [self custom_handleTapAction:sender]; +} + +@end From 1bc351bb439feb5671fffde565b7f6671d92657a Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 19 Jul 2022 17:40:58 +0800 Subject: [PATCH 23/38] fix: Twitter image media extension may not set issue. resolve --- .../Service/PhotoLibraryService.swift | 29 +++++++++++++++++-- TwidereX.xcodeproj/project.pbxproj | 6 +++- TwidereXTests/TwidereXTests+Issue92.swift | 27 +++++++++++++++++ TwidereXTests/TwidereXTests.swift | 9 +++--- 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 TwidereXTests/TwidereXTests+Issue92.swift diff --git a/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift b/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift index 2046b91b..999a25de 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/PhotoLibraryService.swift @@ -13,6 +13,7 @@ import Alamofire import AlamofireImage import TwidereCommon import SwiftMessages +import Kingfisher public final class PhotoLibraryService: NSObject { @@ -101,18 +102,40 @@ extension PhotoLibraryService { let temporaryDirectory = FileManager.default.temporaryDirectory let downloadDirectory = temporaryDirectory.appendingPathComponent("Download", isDirectory: true) try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) + let filename: String = { + switch source { + case .remote(let url): return url.deletingPathExtension().lastPathComponent + case .image: return UUID().uuidString + } + }() let pathExtension: String = { switch source { - case .remote(let url): return url.pathExtension - case .image: return "png" + case .remote(let url): + let pathExtension = url.pathExtension + if pathExtension.isEmpty { + return PhotoLibraryService.pathExtension(for: data) + } else { + return pathExtension + } + case .image: + return "png" // use image png representation } }() - let assetURL = downloadDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false).appendingPathExtension(pathExtension) + let assetURL = downloadDirectory.appendingPathComponent(filename, isDirectory: false).appendingPathExtension(pathExtension) try data.write(to: assetURL) return assetURL } + static func pathExtension(for data: Data) -> String { + switch data.kf.imageFormat { + case .unknown: return "png" // fallback to png + case .PNG: return "png" + case .JPEG: return "jpg" + case .GIF: return "gif" + } + } + } extension PhotoLibraryService { diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 1448d2a7..43703923 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -152,6 +152,7 @@ DB5BF12327F5A549002A3EF5 /* PublishPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF12227F5A549002A3EF5 /* PublishPostIntentHandler.swift */; }; DB5BF12527F5A5C1002A3EF5 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF12427F5A5C1002A3EF5 /* Account+Fetch.swift */; }; DB5BF12D27F5BAD5002A3EF5 /* AuthenticationIndex+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5BF12C27F5BAD5002A3EF5 /* AuthenticationIndex+Fetch.swift */; }; + DB5F1C142886AE2F00978F38 /* TwidereXTests+Issue92.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5F1C132886AE2F00978F38 /* TwidereXTests+Issue92.swift */; }; DB5FB00F2727FCB7006520FA /* SearchUserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67616B254C021A006C6798 /* SearchUserViewController.swift */; }; DB5FB0102727FCC5006520FA /* SearchUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67617A254C0279006C6798 /* SearchUserViewModel.swift */; }; DB5FB0112727FD02006520FA /* SearchUserViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBDDE72D254C084A0057CF8E /* SearchUserViewModel+Diffable.swift */; }; @@ -672,6 +673,7 @@ DB5BF12227F5A549002A3EF5 /* PublishPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishPostIntentHandler.swift; sourceTree = ""; }; DB5BF12427F5A5C1002A3EF5 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; }; DB5BF12C27F5BAD5002A3EF5 /* AuthenticationIndex+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthenticationIndex+Fetch.swift"; sourceTree = ""; }; + DB5F1C132886AE2F00978F38 /* TwidereXTests+Issue92.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwidereXTests+Issue92.swift"; sourceTree = ""; }; DB5FD9B226D8D94600CF5439 /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; DB5FD9B626D8EFF700CF5439 /* UserBriefInfoView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserBriefInfoView+ViewModel.swift"; sourceTree = ""; }; DB60C1A82762394E00628235 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; @@ -2139,9 +2141,10 @@ DBDA8E3724FCF8A7006750DC /* TwidereXTests */ = { isa = PBXGroup; children = ( - DBDA8E3824FCF8A7006750DC /* TwidereXTests.swift */, DBDA8E3A24FCF8A7006750DC /* Info.plist */, DBD40B842599B9C2006E4ABC /* CombineTests.swift */, + DBDA8E3824FCF8A7006750DC /* TwidereXTests.swift */, + DB5F1C132886AE2F00978F38 /* TwidereXTests+Issue92.swift */, ); path = TwidereXTests; sourceTree = ""; @@ -3317,6 +3320,7 @@ files = ( DBDA8E3924FCF8A7006750DC /* TwidereXTests.swift in Sources */, DBD40B852599B9C2006E4ABC /* CombineTests.swift in Sources */, + DB5F1C142886AE2F00978F38 /* TwidereXTests+Issue92.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/TwidereXTests/TwidereXTests+Issue92.swift b/TwidereXTests/TwidereXTests+Issue92.swift new file mode 100644 index 00000000..e89bdc20 --- /dev/null +++ b/TwidereXTests/TwidereXTests+Issue92.swift @@ -0,0 +1,27 @@ +// +// TwidereXTests+Issue92.swift +// TwidereXTests +// +// Created by MainasuK on 2022-7-19. +// Copyright © 2022 Twidere. All rights reserved. +// + +import XCTest +@testable import TwidereX + +// https://github.com/TwidereProject/TwidereX-iOS/issues/92 +extension TwidereXTests { + + @MainActor + func testIssue92() async throws { + let assetURL = URL(string: "https://pbs.twimg.com/media/FX9pNPaUcAATPfh?format=jpg&name=orig")! + + guard let fileURL = try await PhotoLibraryService().file(from: .remote(url: assetURL)) else { + throw AppError.implicit(.internal(reason: "cannot save file")) + } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): url: \(fileURL), pathExtension: \(fileURL.pathExtension)") + XCTAssert(!fileURL.pathExtension.isEmpty, "extension should be valid") + } + +} diff --git a/TwidereXTests/TwidereXTests.swift b/TwidereXTests/TwidereXTests.swift index 72cd4c7c..36a35584 100644 --- a/TwidereXTests/TwidereXTests.swift +++ b/TwidereXTests/TwidereXTests.swift @@ -5,12 +5,13 @@ // Created by Cirno MainasuK on 2020-8-31. // +import os.log import XCTest @testable import TwidereX -import CryptoKit -import CryptoSwift class TwidereXTests: XCTestCase { + + let logger = Logger(subsystem: "TwidereXTests", category: "UnitTest") override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. @@ -34,6 +35,4 @@ class TwidereXTests: XCTestCase { } -extension TwidereXTests { - -} + From 2237e2df0b1a67a9115384956d88ce7c7de26220 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 19 Jul 2022 18:26:38 +0800 Subject: [PATCH 24/38] chore: update version to 1.4.2 (112) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 61 ++++++++++++------------------ TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 30 insertions(+), 43 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 47b0cb6e..98891f08 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 111 + 112 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index a8f866a8..e8ad8398 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 111 + 112 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 43703923..8f01d6a4 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -218,7 +218,6 @@ DB7FF06B28853AB300BFD55E /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05628853A4F00BFD55E /* NotificationService+Decrypt.swift */; }; DB7FF06C28853AB800BFD55E /* String+Escape.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7FF05528853A4F00BFD55E /* String+Escape.swift */; }; DB7FF06E28853B1F00BFD55E /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; }; - DB7FF06F28853B1F00BFD55E /* AppShared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DB94B6BB26C65CB000A2E8A1 /* AppShared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DB8301F7273CED0400BF5224 /* NotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F6273CED0400BF5224 /* NotificationTimelineViewController.swift */; }; DB8301FA273CED2E00BF5224 /* NotificationTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301F9273CED2E00BF5224 /* NotificationTimelineViewModel.swift */; }; DB830200273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8301FF273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift */; }; @@ -467,17 +466,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - DB7FF07228853B1F00BFD55E /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - DB7FF06F28853B1F00BFD55E /* AppShared.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; DBFC0AEB276118240011E99B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -2478,7 +2466,6 @@ DB7FF05A28853A7F00BFD55E /* Sources */, DB7FF05B28853A7F00BFD55E /* Frameworks */, DB7FF05C28853A7F00BFD55E /* Resources */, - DB7FF07228853B1F00BFD55E /* Embed Frameworks */, ); buildRules = ( ); @@ -3563,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3589,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3612,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3638,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 111; + DYLIB_CURRENT_VERSION = 112; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3670,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3697,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3727,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3757,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3786,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3815,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 111; + DYLIB_CURRENT_VERSION = 112; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3849,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 111; + DYLIB_CURRENT_VERSION = 112; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3880,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3907,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4061,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4092,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4118,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4143,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4166,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4189,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4213,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4241,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 111; + CURRENT_PROJECT_VERSION = 112; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 9fac50de..f50c3755 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 111 + 112 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 78d873da..b49737e8 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 111 + 112 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 3fd95d2a..4c357737 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 111 + 112 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 3fd95d2a..4c357737 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 111 + 112 From 35a1983daf2213fcce9eb667f740410ec7097c10 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 21 Jul 2022 18:58:26 +0800 Subject: [PATCH 25/38] feat: fallback the v2 api to v1 when Twitter api usage exceed rate limit --- .../Twitter/Persistence+TwitterPoll.swift | 41 +++-- .../APIService/APIService+Status+List.swift | 58 +++++++ .../APIService/APIService+Status+Search.swift | 85 +++++++++- .../Timeline/APIService+Timeline+Like.swift | 16 +- .../Timeline/APIService+Timeline+User.swift | 16 +- .../Timeline/APIService+Timeline.swift | 110 +++++++++++++ .../StatusFetchViewModel+Timeline+List.swift | 116 +++++++++++-- ...StatusFetchViewModel+Timeline+Search.swift | 150 +++++++++++++---- .../StatusFetchViewModel+Timeline+User.swift | 152 ++++++++++++++---- .../StatusFetchViewModel+Timeline.swift | 13 ++ .../Twitter+API+Error+TwitterAPIError.swift | 2 +- .../TwitterSDK/API/Twitter+API+List.swift | 53 ++++++ .../TwitterSDK/API/Twitter+API+Search.swift | 27 ++-- .../V2/Twitter+Response+V2+DictContent.swift | 2 + ...tatusThreadViewModel+LoadThreadState.swift | 79 ++++++++- .../TimelineViewModel+LoadOldestState.swift | 5 +- .../Base/List/ListTimelineViewModel.swift | 1 + 17 files changed, 808 insertions(+), 118 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterPoll.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterPoll.swift index 0046f100..22b35775 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterPoll.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Twitter/Persistence+TwitterPoll.swift @@ -141,19 +141,38 @@ extension Persistence.TwitterPoll { poll: TwitterPoll, context: PersistContext ) { - let optionEntities = context.entity.options + let optionEntities = context.entity.options.sorted(by: { $0.position < $1.position }) let options = poll.options.sorted(by: { $0.position < $1.position }) - for (option, entity) in zip(options, optionEntities) { - Persistence.TwitterPollOption.merge( - option: option, - context: Persistence.TwitterPollOption.PersistContext( - entity: entity, - me: context.me, - networkDate: context.networkDate + + for entity in optionEntities { + let position = entity.position + for option in options.filter({ $0.position == position }) { + Persistence.TwitterPollOption.merge( + option: option, + context: Persistence.TwitterPollOption.PersistContext( + entity: entity, + me: context.me, + networkDate: context.networkDate + ) ) - ) - } // end for in - + } + } + + // remove duplicated options + // + // note: + // some dev builds intro duplicated options + // the release builds could just ignore it + if let managedObjectContext = poll.managedObjectContext { + let positions = Set(options.map { $0.position }) + for position in positions { + let optionsAtPosition = options.filter { $0.position == position } + optionsAtPosition.dropFirst().forEach { option in + managedObjectContext.delete(option) + } + } + } + poll.update(updatedAt: context.networkDate) } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift index 2b033068..235b3ac5 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+List.swift @@ -5,10 +5,12 @@ // Created by MainasuK on 2022-3-2. // +import os.log import Foundation import CoreDataStack import TwitterSDK import MastodonSDK +import func QuartzCore.CACurrentMediaTime extension APIService { @@ -45,6 +47,62 @@ extension APIService { return response } + public func twitterListStatusesV1( + query: Twitter.API.List.StatusesQuery, + authenticationContext: TwitterAuthenticationContext + ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { + let response = try await Twitter.API.List.statuses( + session: session, + query: query, + authorization: authenticationContext.authorization + ) + + let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } + let _lookupResponse = try? await twitterBatchLookupV2( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + #if DEBUG + // log time cost + let start = CACurrentMediaTime() + defer { + // log rate limit + response.logRateLimit() + + let end = CACurrentMediaTime() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) + } + #endif + + let managedObjectContext = backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + + var statusArray: [TwitterStatus] = [] + for entity in response.value { + let result = Persistence.TwitterStatus.createOrMerge( + in: managedObjectContext, + context: .init( + entity: entity, + me: me, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + statusArray.append(result.status) + } // end for in + + // amend the v2 only properties + if let lookupResponse = _lookupResponse, let me = me { + lookupResponse.update(statuses: statusArray, me: me) + } + } + + return response + } + } extension APIService { diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift index 5693f907..07fcfce7 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Search.swift @@ -22,7 +22,88 @@ extension APIService { } -// MARK: - Twitter +// MARK: - Twitter V1 +extension APIService { + + // for thread + public func searchTwitterStatusV1( + conversationRootTweetID: Twitter.Entity.Tweet.ID, + authorUsername: String, + maxID: String?, + authenticationContext: TwitterAuthenticationContext + ) async throws -> Twitter.Response.Content { + let query = Twitter.API.Statuses.Timeline.TimelineQuery( + count: APIService.conversationSearchCount, + maxID: maxID, + sinceID: conversationRootTweetID, + query: "to:\(authorUsername) OR from:\(authorUsername) -filter:retweets" + ) + return try await searchTwitterStatusV1( + query: query, + authenticationContext: authenticationContext + ) + } + + // for search + public func searchTwitterStatusV1( + query: Twitter.API.Statuses.Timeline.TimelineQuery, + authenticationContext: TwitterAuthenticationContext + ) async throws -> Twitter.Response.Content { + let response = try await Twitter.API.Search.tweets( + session: session, + query: query, + authorization: authenticationContext.authorization + ) + + let statusIDs: [Twitter.Entity.V2.Tweet.ID] = (response.value.statuses ?? []).map { $0.idStr } + let _lookupResponse = try? await twitterBatchLookupV2( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + #if DEBUG + // log time cost + let start = CACurrentMediaTime() + defer { + // log rate limit + response.logRateLimit() + + let end = CACurrentMediaTime() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: persist cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start) + } + #endif + + let managedObjectContext = backgroundManagedObjectContext + try await managedObjectContext.performChanges { + let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + + var statusArray: [TwitterStatus] = [] + for entity in response.value.statuses ?? [] { + let result = Persistence.TwitterStatus.createOrMerge( + in: managedObjectContext, + context: .init( + entity: entity, + me: me, + statusCache: nil, + userCache: nil, + networkDate: response.networkDate + ) + ) + statusArray.append(result.status) + } // end for in + + // amend the v2 only properties + if let lookupResponse = _lookupResponse, let me = me { + lookupResponse.update(statuses: statusArray, me: me) + } + } + + return response + } + +} + +// MARK: - Twitter V2 extension APIService { // for search @@ -101,7 +182,7 @@ extension APIService { try await managedObjectContext.performChanges { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user - Persistence.Twitter.persist( + _ = Persistence.Twitter.persist( in: managedObjectContext, context: Persistence.Twitter.PersistContextV2( dictionary: dictionary, 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 289e178d..cd002269 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+Like.swift @@ -99,6 +99,12 @@ extension APIService { authorization: authenticationContext.authorization ) + let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } + let _lookupResponse = try? await twitterBatchLookupV2( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -114,7 +120,9 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext try await managedObjectContext.performChanges { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + // persist status + var statusArray: [TwitterStatus] = [] for entity in response.value { let persistContext = Persistence.TwitterStatus.PersistContext( entity: entity, @@ -123,10 +131,16 @@ extension APIService { userCache: nil, networkDate: response.networkDate ) - let _ = Persistence.TwitterStatus.createOrMerge( + let result = Persistence.TwitterStatus.createOrMerge( in: managedObjectContext, context: persistContext ) + statusArray.append(result.status) + } + + // amend the v2 only properties + if let lookupResponse = _lookupResponse, let me = me { + lookupResponse.update(statuses: statusArray, me: me) } } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift index ded28bae..6a6e13ff 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline+User.swift @@ -97,6 +97,12 @@ extension APIService { authorization: authenticationContext.authorization ) + let statusIDs: [Twitter.Entity.V2.Tweet.ID] = response.value.map { $0.idStr } + let _lookupResponse = try? await twitterBatchLookupV2( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + #if DEBUG // log time cost let start = CACurrentMediaTime() @@ -112,7 +118,9 @@ extension APIService { let managedObjectContext = backgroundManagedObjectContext try await managedObjectContext.performChanges { let me = authenticationContext.authenticationRecord.object(in: managedObjectContext)?.user + // persist TwitterStatus + var statusArray: [TwitterStatus] = [] for entity in response.value { let persistContext = Persistence.TwitterStatus.PersistContext( entity: entity, @@ -121,10 +129,16 @@ extension APIService { userCache: nil, networkDate: response.networkDate ) - let _ = Persistence.TwitterStatus.createOrMerge( + let result = Persistence.TwitterStatus.createOrMerge( in: managedObjectContext, context: persistContext ) + statusArray.append(result.status) + } + + // amend the v2 only properties + if let lookupResponse = _lookupResponse, let me = me { + lookupResponse.update(statuses: statusArray, me: me) } } diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift index ac3a248c..81955e96 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/Timeline/APIService+Timeline.swift @@ -232,3 +232,113 @@ extension APIService { } +// Fetch v2 API again to update v2 only properies +extension APIService { + + public struct TwitterBatchLookupResponseV2 { + let logger = Logger(subsystem: "APIService", category: "TwitterBatchLookupResponseV2") + + let dictionary: Twitter.Response.V2.DictContent + + public func update(status: TwitterStatus, me: TwitterUser) { + guard let lookupStatus = dictionary.tweetDict[status.id] else { return } + guard let managedObjectContext = status.managedObjectContext else { return } + + let now = Date() + + // poll + if let poll = dictionary.poll(for: lookupStatus) { + let result = Persistence.TwitterPoll.createOrMerge( + in: managedObjectContext, + context: .init( + entity: poll, + me: me, + networkDate: now + ) + ) + status.attach(poll: result.poll) + } + + // reply settings + if let value = lookupStatus.replySettings { + let replySettings = TwitterReplySettings(value: value.rawValue) + status.update(replySettings: replySettings) + } + } + + public func update(statuses: [TwitterStatus], me: TwitterUser) { + for status in statuses { + update(status: status, me: me) + } + } + } + + public func twitterBatchLookupResponsesV2( + statusIDs: [Twitter.Entity.V2.Tweet.ID], + authenticationContext: TwitterAuthenticationContext + ) async -> [Twitter.Response.Content] { + let chunks = stride(from: 0, to: statusIDs.count, by: 100).map { + statusIDs[$0.. Twitter.Response.Content? in + let response = try? await Twitter.API.V2.Lookup.statuses( + session: self.session, + query: .init(statusIDs: Array(chunk)), + authorization: authenticationContext.authorization + ) + return response + } + + return _responses.compactMap { $0 } + } + + public func twitterBatchLookupV2( + statusIDs: [Twitter.Entity.V2.Tweet.ID], + authenticationContext: TwitterAuthenticationContext + ) async throws -> TwitterBatchLookupResponseV2 { + let responses = await twitterBatchLookupResponsesV2( + statusIDs: statusIDs, + authenticationContext: authenticationContext + ) + + var tweets: [Twitter.Entity.V2.Tweet] = [] + var users: [Twitter.Entity.V2.User] = [] + var media: [Twitter.Entity.V2.Media] = [] + var places: [Twitter.Entity.V2.Place] = [] + var polls: [Twitter.Entity.V2.Tweet.Poll] = [] + + for response in responses { + if let value = response.value.data { + tweets.append(contentsOf: value) + } + if let value = response.value.includes?.tweets { + tweets.append(contentsOf: value) + } + if let value = response.value.includes?.users { + users.append(contentsOf: value) + } + if let value = response.value.includes?.media { + media.append(contentsOf: value) + } + if let value = response.value.includes?.places { + places.append(contentsOf: value) + } + if let value = response.value.includes?.polls { + polls.append(contentsOf: value) + } + } + + let dictionary = Twitter.Response.V2.DictContent( + tweets: tweets, + users: users, + media: media, + places: places, + polls: polls + ) + + return .init(dictionary: dictionary) + } + +} + diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift index 9f9858e2..f4fd0517 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+List.swift @@ -28,19 +28,24 @@ extension StatusFetchViewModel.Timeline.List { public let authenticationContext: TwitterAuthenticationContext public let list: ManagedObjectRecord public let paginationToken: String? + public let maxID: Twitter.Entity.Tweet.ID? public let maxResults: Int? public let filter: StatusFetchViewModel.Timeline.Filter + public var needsAPIFallback = false + public init( authenticationContext: TwitterAuthenticationContext, list: ManagedObjectRecord, paginationToken: String?, + maxID: Twitter.Entity.Tweet.ID?, maxResults: Int?, filter: StatusFetchViewModel.Timeline.Filter ) { self.authenticationContext = authenticationContext self.list = list self.paginationToken = paginationToken + self.maxID = maxID self.maxResults = maxResults self.filter = filter } @@ -50,6 +55,18 @@ extension StatusFetchViewModel.Timeline.List { authenticationContext: authenticationContext, list: list, paginationToken: paginationToken, + maxID: maxID, + maxResults: maxResults, + filter: filter + ) + } + + func map(maxID: Twitter.Entity.Tweet.ID) -> TwitterFetchContext { + return TwitterFetchContext( + authenticationContext: authenticationContext, + list: list, + paginationToken: paginationToken, + maxID: maxID, maxResults: maxResults, filter: filter ) @@ -107,30 +124,97 @@ extension StatusFetchViewModel.Timeline.List { extension StatusFetchViewModel.Timeline.List { - public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { - switch input { - case .twitter(let fetchContext): - let query = Twitter.API.V2.Status.List.StatusesQuery( - maxResults: fetchContext.maxResults ?? 20, - nextToken: fetchContext.paginationToken - ) - let response = try await api.twitterListStatuses( - list: fetchContext.list, - query: query, - authenticationContext: fetchContext.authenticationContext - ) - let backInput: Input? = { + enum TwitterResponse { + case v2(Twitter.Response.Content) + case v1(Twitter.Response.Content<[Twitter.Entity.Tweet]>) + + func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { + switch self { + case .v2(let response): + let statuses = response.value.data ?? [] + let result = statuses.filter(fetchContext.filter.isIncluded) + return .twitterV2(result) + case .v1(let response): + let result = response.value.filter(fetchContext.filter.isIncluded) + return .twitter(result) + } + } + + func backInput(fetchContext: TwitterFetchContext) -> Input? { + switch self { + case .v2(let response): guard let nextToken = response.value.meta.previousToken else { return nil } let fetchContext = fetchContext.map(paginationToken: nextToken) return .twitter(fetchContext) - }() - let nextInput: Input? = { + case .v1: + return nil + } + } + + func nextInput(fetchContext: TwitterFetchContext) -> Input? { + switch self { + case .v2(let response): guard let nextToken = response.value.meta.nextToken else { return nil } let fetchContext = fetchContext.map(paginationToken: nextToken) return .twitter(fetchContext) + case .v1(let response): + guard let maxID = response.value.last?.idStr else { return nil } + guard maxID != fetchContext.maxID else { return nil } + var fetchContext = fetchContext.map(maxID: maxID) + fetchContext.needsAPIFallback = true + return .twitter(fetchContext) + } + } + } + + public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { + switch input { + case .twitter(let fetchContext): + let response: TwitterResponse = try await { + do { + guard !fetchContext.needsAPIFallback else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } + let query = Twitter.API.V2.Status.List.StatusesQuery( + maxResults: fetchContext.maxResults ?? 20, + nextToken: fetchContext.paginationToken + ) + let response = try await api.twitterListStatuses( + list: fetchContext.list, + query: query, + authenticationContext: fetchContext.authenticationContext + ) + return .v2(response) + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") + let managedObjectContext = api.backgroundManagedObjectContext + let _listID: TwitterList.ID? = await managedObjectContext.perform { + guard let list = fetchContext.list.object(in: managedObjectContext) else { return nil } + return list.id + } + guard let listID = _listID else { + throw AppError.implicit(.badRequest) + } + let response = try await api.twitterListStatusesV1( + query: .init( + id: listID, + maxID: fetchContext.maxID + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v1(response) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + throw error + } }() + + let backInput = response.backInput(fetchContext: fetchContext) + let nextInput = response.nextInput(fetchContext: fetchContext) + let result = response.filter(fetchContext: fetchContext) + return .init( - result: .twitterV2(response.value.data ?? []), + result: result, backInput: backInput.flatMap { .list($0) }, nextInput: nextInput.flatMap { .list($0) } ) diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift index 140ad64f..3354d29d 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+Search.swift @@ -29,14 +29,18 @@ extension StatusFetchViewModel.Timeline.Search { public let searchText: String public let untilID: Twitter.Entity.V2.Tweet.ID? public let nextToken: String? + public let maxID: Twitter.Entity.Tweet.ID? public let maxResults: Int? public let filter: StatusFetchViewModel.Timeline.Filter + public var needsAPIFallback = false + public init( authenticationContext: TwitterAuthenticationContext, searchText: String, untilID: Twitter.Entity.V2.Tweet.ID?, nextToken: String?, + maxID: Twitter.Entity.Tweet.ID?, maxResults: Int?, filter: StatusFetchViewModel.Timeline.Filter ) { @@ -44,6 +48,7 @@ extension StatusFetchViewModel.Timeline.Search { self.searchText = searchText self.untilID = untilID self.nextToken = nextToken + self.maxID = maxID self.maxResults = maxResults self.filter = filter } @@ -54,6 +59,19 @@ extension StatusFetchViewModel.Timeline.Search { searchText: searchText, untilID: untilID, nextToken: nextToken, + maxID: maxID, + maxResults: maxResults, + filter: filter + ) + } + + func map(maxID: Twitter.Entity.Tweet.ID) -> TwitterFetchContext { + return TwitterFetchContext( + authenticationContext: authenticationContext, + searchText: searchText, + untilID: untilID, + nextToken: nextToken, + maxID: maxID, maxResults: maxResults, filter: filter ) @@ -96,43 +114,113 @@ extension StatusFetchViewModel.Timeline.Search { extension StatusFetchViewModel.Timeline.Search { + enum TwitterResponse { + case v2(Twitter.Response.Content) + case v1(Twitter.Response.Content) + + func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { + switch self { + case .v2(let response): + let statuses = response.value.data ?? [] + let result = statuses.filter(fetchContext.filter.isIncluded) + return .twitterV2(result) + case .v1(let response): + let result = (response.value.statuses ?? []).filter(fetchContext.filter.isIncluded) + return .twitter(result) + } + } + + func nextInput(fetchContext: TwitterFetchContext) -> Input? { + switch self { + case .v2(let response): + guard let nextToken = response.value.meta.nextToken else { return nil } + let fetchContext = fetchContext.map(untilID: response.value.meta.oldestID, nextToken: nextToken) + return .twitter(fetchContext) + case .v1(let response): + guard let maxID = response.value.statuses?.last?.idStr else { return nil } + guard maxID != fetchContext.maxID else { return nil } + var fetchContext = fetchContext.map(maxID: maxID) + fetchContext.needsAPIFallback = true + return .twitter(fetchContext) + } + } + } + public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): - let queryText: String = try { - let searchText = fetchContext.searchText.trimmingCharacters(in: .whitespacesAndNewlines) - guard !searchText.isEmpty && searchText.count < 500 else { - throw AppError.implicit(.badRequest) + let response: TwitterResponse = try await { + do { + guard !fetchContext.needsAPIFallback else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } + let queryText: String = try { + let searchText = fetchContext.searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !searchText.isEmpty && searchText.count < 500 else { + throw AppError.implicit(.badRequest) + } + var query = searchText + // default exclude retweet + var options = ["-is:retweet"] + if fetchContext.filter.rule.contains(.onlyMedia) { + options.append("has:media") + } + // TODO: more options + let suffix = options.joined(separator: " ") + query += " (\(suffix))" + return query + }() + let query = Twitter.API.V2.Search.RecentTweetQuery( + query: queryText, + maxResults: fetchContext.maxResults ?? 100, + sinceID: nil, + startTime: nil, + nextToken: fetchContext.nextToken + ) + let response = try await api.searchTwitterStatus( + query: query, + authenticationContext: fetchContext.authenticationContext + ) + return .v2(response) + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") + let queryText: String = try { + let searchText = fetchContext.searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !searchText.isEmpty && searchText.count < 500 else { + throw AppError.implicit(.badRequest) + } + var query = searchText + // default exclude retweet + var options = ["-filter:retweets"] + if fetchContext.filter.rule.contains(.onlyMedia) { + options.append("filter:media") + } + // TODO: more options + let suffix = options.joined(separator: " ") + query += " (\(suffix))" + return query + }() + let response = try await api.searchTwitterStatusV1( + query: .init( + count: fetchContext.maxResults ?? 100, + userID: nil, + maxID: fetchContext.maxID, + sinceID: nil, + excludeReplies: nil, + query: queryText + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v1(response) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + throw error } - var query = searchText - // default exclude retweet - var options = ["-is:retweet"] - if fetchContext.filter.rule.contains(.onlyMedia) { - options.append("has:media") - } - // TODO: more options - let suffix = options.joined(separator: " ") - query += " (\(suffix))" - return query - }() - let query = Twitter.API.V2.Search.RecentTweetQuery( - query: queryText, - maxResults: fetchContext.maxResults ?? 100, - sinceID: nil, - startTime: nil, - nextToken: fetchContext.nextToken - ) - let response = try await api.searchTwitterStatus( - query: query, - authenticationContext: fetchContext.authenticationContext - ) - let nextInput: Input? = { - guard let nextToken = response.value.meta.nextToken else { return nil } - let fetchContext = fetchContext.map(untilID: response.value.meta.oldestID, nextToken: nextToken) - return .twitter(fetchContext) }() + let result = response.filter(fetchContext: fetchContext) + let nextInput = response.nextInput(fetchContext: fetchContext) return .init( - result: .twitterV2(response.value.data ?? []), + result: result, backInput: nil, nextInput: nextInput.flatMap { .search($0) } ) diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift index 10f4c627..23981b60 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline+User.swift @@ -27,14 +27,18 @@ extension StatusFetchViewModel.Timeline.User { public let authenticationContext: TwitterAuthenticationContext public let userID: Twitter.Entity.V2.User.ID public let paginationToken: String? + public let maxID: Twitter.Entity.V2.Tweet.ID? public let maxResults: Int? public let filter: StatusFetchViewModel.Timeline.Filter public let timelineKind: StatusFetchViewModel.Timeline.Kind.UserTimelineContext.TimelineKind + public var needsAPIFallback: Bool = false + public init( authenticationContext: TwitterAuthenticationContext, userID: Twitter.Entity.V2.User.ID, paginationToken: String?, + maxID: Twitter.Entity.V2.Tweet.ID?, maxResults: Int?, filter: StatusFetchViewModel.Timeline.Filter, timelineKind: StatusFetchViewModel.Timeline.Kind.UserTimelineContext.TimelineKind @@ -42,6 +46,7 @@ extension StatusFetchViewModel.Timeline.User { self.authenticationContext = authenticationContext self.userID = userID self.paginationToken = paginationToken + self.maxID = maxID self.maxResults = maxResults self.filter = filter self.timelineKind = timelineKind @@ -52,6 +57,19 @@ extension StatusFetchViewModel.Timeline.User { authenticationContext: authenticationContext, userID: userID, paginationToken: paginationToken, + maxID: maxID, + maxResults: maxResults, + filter: filter, + timelineKind: timelineKind + ) + } + + func map(maxID: Twitter.Entity.V2.Tweet.ID) -> TwitterFetchContext { + return TwitterFetchContext( + authenticationContext: authenticationContext, + userID: userID, + paginationToken: paginationToken, + maxID: maxID, maxResults: maxResults, filter: filter, timelineKind: timelineKind @@ -98,47 +116,117 @@ extension StatusFetchViewModel.Timeline.User { } extension StatusFetchViewModel.Timeline.User { + + enum TwitterResponse { + case v2(Twitter.Response.Content) + case v1(Twitter.Response.Content<[Twitter.Entity.Tweet]>) + + func filter(fetchContext: TwitterFetchContext) -> StatusFetchViewModel.Result { + switch self { + case .v2(let response): + let statuses = response.value.data ?? [] + let result = statuses.filter(fetchContext.filter.isIncluded) + return .twitterV2(result) + case .v1(let response): + let result = response.value.filter(fetchContext.filter.isIncluded) + return .twitter(result) + } + } + + func nextInput(fetchContext: TwitterFetchContext) -> Input? { + switch self { + case .v2(let response): + guard let nextToken = response.value.meta.nextToken else { return nil } + let fetchContext = fetchContext.map(paginationToken: nextToken) + return .twitter(fetchContext) + case .v1(let response): + guard let maxID = response.value.last?.idStr else { return nil } + guard maxID != fetchContext.maxID else { return nil } + var fetchContext = fetchContext.map(maxID: maxID) + fetchContext.needsAPIFallback = true + return .twitter(fetchContext) + } + } + } public static func fetch(api: APIService, input: Input) async throws -> StatusFetchViewModel.Timeline.Output { switch input { case .twitter(let fetchContext): - let response: Twitter.Response.Content = try await { + let response: TwitterResponse = try await { switch fetchContext.timelineKind { case .status, .media: - return try await api.twitterUserTimeline( - userID: fetchContext.userID, - query: .init( - sinceID: nil, - untilID: nil, - paginationToken: fetchContext.paginationToken, - maxResults: fetchContext.maxResults ?? 20 - ), - authenticationContext: fetchContext.authenticationContext - ) + do { + guard !fetchContext.needsAPIFallback else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } + let response = try await api.twitterUserTimeline( + userID: fetchContext.userID, + query: .init( + sinceID: nil, + untilID: nil, + paginationToken: fetchContext.paginationToken, + maxResults: fetchContext.maxResults ?? 20 + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v2(response) + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") + let response = try await api.twitterUserTimelineV1( + query: .init( + count: fetchContext.maxResults ?? 20, + userID: fetchContext.userID, + maxID: fetchContext.maxID, + sinceID: nil, + excludeReplies: false, + query: nil + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v1(response) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + throw error + } case .like: - return try await api.twitterLikeTimeline( - userID: fetchContext.userID, - query: .init( - sinceID: nil, - untilID: nil, - paginationToken: fetchContext.paginationToken, - maxResults: fetchContext.maxResults ?? 20 - ), - authenticationContext: fetchContext.authenticationContext - ) - } + do { + guard !fetchContext.needsAPIFallback else { + throw Twitter.API.Error.ResponseError(httpResponseStatus: .ok, twitterAPIError: .rateLimitExceeded) + } + let response = try await api.twitterLikeTimeline( + userID: fetchContext.userID, + query: .init( + sinceID: nil, + untilID: nil, + paginationToken: fetchContext.paginationToken, + maxResults: fetchContext.maxResults ?? 20 + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v2(response) + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Rate Limit] fallback to v1") + let response = try await api.twitterLikeTimelineV1( + query: .init( + count: fetchContext.maxResults ?? 20, + userID: fetchContext.userID, + maxID: fetchContext.maxID, + sinceID: nil, + excludeReplies: false, + query: nil + ), + authenticationContext: fetchContext.authenticationContext + ) + return .v1(response) + } catch { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch failure: \(error.localizedDescription)") + throw error + } + } // end switch }() // filter result - let reulst: StatusFetchViewModel.Result = { - let statuses = response.value.data ?? [] - let result = statuses.filter(fetchContext.filter.isIncluded) - return .twitterV2(result) - }() - let nextInput: Input? = { - guard let nextToken = response.value.meta.nextToken else { return nil } - let fetchContext = fetchContext.map(paginationToken: nextToken) - return .twitter(fetchContext) - }() + let reulst = response.filter(fetchContext: fetchContext) + let nextInput = response.nextInput(fetchContext: fetchContext) return .init( result: reulst, backInput: nil, diff --git a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift index 7b3a036c..38f48e09 100644 --- a/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift +++ b/TwidereSDK/Sources/TwidereCore/ViewModel/Status/StatusFetchViewModel+Timeline.swift @@ -176,6 +176,15 @@ extension StatusFetchViewModel.Timeline { return true } + + public func isIncluded(_ status: Twitter.Entity.Tweet) -> Bool { + if rule.contains(.onlyMedia) { + // the repost also contains the mediaKeys. Not needs handle explicitly + guard let media = status.extendedEntities?.media, !media.isEmpty else { return false } + } + + return true + } } } @@ -320,6 +329,7 @@ extension StatusFetchViewModel.Timeline { searchText: hashtag, untilID: nil, nextToken: nil, + maxID: nil, maxResults: nil, filter: fetchContext.filter ))) @@ -365,6 +375,7 @@ extension StatusFetchViewModel.Timeline { authenticationContext: authenticationContext, list: record, paginationToken: nil, + maxID: nil, maxResults: nil, filter: fetchContext.filter ))) @@ -389,6 +400,7 @@ extension StatusFetchViewModel.Timeline { searchText: searchTimelineContext.searchText, untilID: nil, nextToken: nil, + maxID: nil, maxResults: nil, filter: { switch searchTimelineContext.timelineKind { @@ -427,6 +439,7 @@ extension StatusFetchViewModel.Timeline { authenticationContext: authenticationContext, userID: userIdentifier.id, paginationToken: nil, + maxID: nil, maxResults: nil, filter: { switch userTimelineContext.timelineKind { diff --git a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift b/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift index f4a0ce02..d8ac4ec1 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Error/Twitter+API+Error+TwitterAPIError.swift @@ -10,7 +10,7 @@ import Foundation // Ref: https://developer.twitter.com/en/support/twitter-api/error-troubleshooting // Ref: https://developer.twitter.com/ja/docs/basics/response-codes (prefer) extension Twitter.API.Error { - public enum TwitterAPIError: Error { + public enum TwitterAPIError: Error, Hashable { case custom(code: Int, message: String) diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift index 9a393bcc..299bf6f7 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+List.swift @@ -8,6 +8,7 @@ import Foundation import Combine +// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-show extension Twitter.API.List { static let showEndpointURL = Twitter.API.endpointURL @@ -52,3 +53,55 @@ extension Twitter.API.List { } } + +// https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/api-reference/get-lists-statuses +extension Twitter.API.List { + + static let statusesEndpointURL = Twitter.API.endpointURL + .appendingPathComponent("lists") + .appendingPathComponent("statuses.json") + + public static func statuses( + session: URLSession, + query: StatusesQuery, + authorization: Twitter.API.OAuth.Authorization + ) async throws -> Twitter.Response.Content<[Twitter.Entity.Tweet]> { + let request = Twitter.API.request( + url: statusesEndpointURL, + method: .GET, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Twitter.API.decode(type: [Twitter.Entity.Tweet].self, from: data, response: response) + return Twitter.Response.Content(value: value, response: response) + } + + public struct StatusesQuery: Query { + public let id: Twitter.Entity.List.ID + public let maxID: Twitter.Entity.Tweet.ID? + + public init( + id: Twitter.Entity.List.ID, + maxID: Twitter.Entity.Tweet.ID? + ) { + self.id = id + self.maxID = maxID + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + items.append(URLQueryItem(name: "list_id", value: id)) + maxID.flatMap { + items.append(URLQueryItem(name: "max_id", value: $0)) + } + guard !items.isEmpty else { return nil } + return items + } + var encodedQueryItems: [URLQueryItem]? { nil } + var formQueryItems: [URLQueryItem]? { nil } + var contentType: String? { nil } + var body: Data? { nil } + } + +} diff --git a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift index d897fe1c..e9626969 100644 --- a/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift +++ b/TwidereSDK/Sources/TwitterSDK/API/Twitter+API+Search.swift @@ -14,19 +14,20 @@ extension Twitter.API.Search { public static func tweets( session: URLSession, - authorization: Twitter.API.OAuth.Authorization, - query: Twitter.API.Statuses.Timeline.TimelineQuery - ) -> AnyPublisher, Error> { - let url = tweetsEndpointURL - let request = Twitter.API.request(url: url, httpMethod: "GET", authorization: authorization, queryItems: query.queryItems, encodedQueryItems: query.encodedQueryItems) - return session.dataTaskPublisher(for: request) - .tryMap { data, response in - let value = try Twitter.API.decode(type: Twitter.API.Search.Content.self, from: data, response: response) - return Twitter.Response.Content(value: value, response: response) - } - .eraseToAnyPublisher() + query: Twitter.API.Statuses.Timeline.TimelineQuery, + authorization: Twitter.API.OAuth.Authorization + ) async throws -> Twitter.Response.Content { + let request = Twitter.API.request( + url: tweetsEndpointURL, + method: .GET, + query: query, + authorization: authorization + ) + let (data, response) = try await session.data(for: request, delegate: nil) + let value = try Twitter.API.decode(type: Twitter.API.Search.Content.self, from: data, response: response) + return Twitter.Response.Content(value: value, response: response) } - + } extension Twitter.API.Search { @@ -41,7 +42,7 @@ extension Twitter.API.Search { } public struct SearchMetadata: Codable { - public let nextResults: String + public let nextResults: String? public let query: String public let count: Int diff --git a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift b/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift index 84371b39..e59de593 100644 --- a/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift +++ b/TwidereSDK/Sources/TwitterSDK/Response/V2/Twitter+Response+V2+DictContent.swift @@ -9,6 +9,7 @@ import Foundation import OrderedCollections extension Twitter.Response.V2 { + public class DictContent { public let tweetDict: OrderedDictionary public let userDict: OrderedDictionary @@ -86,4 +87,5 @@ extension Twitter.Response.V2.DictContent { guard let pollID = tweet.attachments?.pollIDs?.first else { return nil } return pollDict[pollID] } + } diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift index 726534b8..740b6d2b 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift @@ -136,7 +136,7 @@ extension StatusThreadViewModel.LoadThreadState { } func prepareMastodonStatusThread(record: ManagedObjectRecord) async { - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel = viewModel else { return } let managedObjectContext = viewModel.context.managedObjectContext let _mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext? = await managedObjectContext.perform { @@ -207,6 +207,9 @@ extension StatusThreadViewModel.LoadThreadState { class Loading: StatusThreadViewModel.LoadThreadState { override var name: String { "Loading" } + var needsFallback = false + + var maxID: String? // v1 var nextToken: String? // v2 override func isValidNextState(_ stateClass: AnyClass) -> Bool { @@ -223,7 +226,7 @@ extension StatusThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let viewModel = viewModel else { return } guard let threadContext = viewModel.threadContext.value else { assertionFailure() return @@ -232,8 +235,13 @@ extension StatusThreadViewModel.LoadThreadState { Task { switch threadContext { case .twitter(let twitterConversation): - let nodes = await fetch(twitterConversation: twitterConversation) - await append(nodes: nodes) + if needsFallback { + let nodes = await fetchFallback(twitterConversation: twitterConversation) + await append(nodes: nodes) + } else { + let nodes = await fetch(twitterConversation: twitterConversation) + await append(nodes: nodes) + } case .mastodon(let mastodonContext): let response = await fetch(mastodonContext: mastodonContext) await append(response: response) @@ -284,7 +292,59 @@ extension StatusThreadViewModel.LoadThreadState { } else { hasMore = false } - + + if hasMore { + await enter(state: Idle.self) + } else { + await enter(state: NoMore.self) + } + + return nodes + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + self.needsFallback = true + stateMachine.enter(Idle.self) + stateMachine.enter(Loading.self) + return [] + } catch { + await enter(state: Fail.self) + return [] + } + } + + // fetch thread via V1 API + func fetchFallback( + twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation + ) async -> [TwitterStatusThreadLeafViewModel.Node] { + guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext, + let _ = twitterConversation.conversationID + else { + await enter(state: Fail.self) + return [] + } + + do { + let response = try await viewModel.context.apiService.searchTwitterStatusV1( + conversationRootTweetID: twitterConversation.statusID, + authorUsername: twitterConversation.authorUsername, + maxID: maxID, + authenticationContext: authenticationContext + ) + let nodes = TwitterStatusThreadLeafViewModel.Node.children( + of: twitterConversation.statusID, + from: response.value + ) + + var hasMore = false + if let nextResult = response.value.searchMetadata.nextResults, + let components = URLComponents(string: nextResult), + let maxID = components.queryItems?.first(where: { $0.name == "max_id" })?.value, + maxID != self.maxID + { + self.maxID = maxID + hasMore = !(response.value.statuses ?? []).isEmpty + } + if hasMore { await enter(state: Idle.self) } else { @@ -292,6 +352,11 @@ extension StatusThreadViewModel.LoadThreadState { } return nodes + } catch let error as Twitter.API.Error.ResponseError where error.twitterAPIError == .rateLimitExceeded { + self.needsFallback = true + stateMachine.enter(Idle.self) + stateMachine.enter(Loading.self) + return [] } catch { await enter(state: Fail.self) return [] @@ -329,7 +394,7 @@ extension StatusThreadViewModel.LoadThreadState { func fetch( mastodonContext: StatusThreadViewModel.ThreadContext.MastodonContext ) async -> MastodonContextResponse { - guard let viewModel = viewModel, let stateMachine = stateMachine else { + guard let viewModel = viewModel else { return MastodonContextResponse( domain: "", ancestorNodes: [], @@ -393,7 +458,6 @@ extension StatusThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel else { return } } } @@ -406,7 +470,6 @@ extension StatusThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel else { return } } } diff --git a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift index c0b4a7d8..fb6b80b8 100644 --- a/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/Common/TimelineViewModel+LoadOldestState.swift @@ -177,14 +177,15 @@ extension TimelineViewModel.LoadOldestState { break case .hashtag, .public, .list, .search, .user: switch output.result { + case .twitter(let statuses): + let statusIDs = statuses.map { $0.idStr } + viewModel.statusRecordFetchedResultController.twitterStatusFetchedResultController.append(statusIDs: statusIDs) case .twitterV2(let statuses): let statusIDs = statuses.map { $0.id } viewModel.statusRecordFetchedResultController.twitterStatusFetchedResultController.append(statusIDs: statusIDs) case .mastodon(let statuses): let statusIDs = statuses.map { $0.id } viewModel.statusRecordFetchedResultController.mastodonStatusFetchedResultController.append(statusIDs: statusIDs) - default: - assertionFailure() } } } catch { diff --git a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift index 62261dde..e6485cb1 100644 --- a/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Base/List/ListTimelineViewModel.swift @@ -7,6 +7,7 @@ // import UIKit +import TwidereCore class ListTimelineViewModel: TimelineViewModel { From aa4a9145fc91cc7c4b4cf626e4b93f7b5dd2763e Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 21 Jul 2022 19:05:33 +0800 Subject: [PATCH 26/38] fix: search scope does not reset after first search action issue --- .../Search/Search/SearchViewController.swift | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 8a4867c7..2a4165a5 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -79,50 +79,6 @@ extension SearchViewController { super.viewDidLoad() drawerSidebarTransitionController = DrawerSidebarTransitionController(hostViewController: self) - -// viewModel.trendViewModel.$twitterTrendPlaces -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak self] places in -// guard let self = self else { return } -// self.navigationItem.rightBarButtonItem = places.isEmpty ? nil : self.trendPreferenceBarButtonItem -// self.trendPreferenceBarButtonItem.menu = { -// let worldwideMenu = UIMenu( -// title: "", -// image: nil, -// identifier: nil, -// options: [.displayInline], -// children: [ -// UIAction( -// title: L10n.Scene.Trends.worldWideWithoutPrefix, -// image: UIImage(systemName: "globe"), -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// self.viewModel.trendViewModel.resetTrendGroupIndex() -// } -// ] -// ) -// let placeActions: [UIMenuElement] = places.map { place in -// UIAction( -// title: place.name, -// image: nil, -// identifier: nil, -// discoverabilityTitle: nil, -// attributes: [], -// state: .off -// ) { [weak self] _ in -// guard let self = self else { return } -// self.viewModel.trendViewModel.updateTrendGroupIndex(place: place) -// } -// } -// return UIMenu(title: "Trend Places", image: nil, identifier: nil, options: [], children: [worldwideMenu] + placeActions) -// }() -// } -// .store(in: &disposeBag) view.backgroundColor = .systemGroupedBackground navigationItem.searchController = searchController @@ -353,6 +309,7 @@ extension SearchViewController { private func searchText(_ text: String) { searchController.isActive = true searchController.searchBar.text = text + searchResultViewController.viewModel.selectedScope = searchResultViewController.viewModel.scopes.first } } From 46e327c0422a262f6324d1057bf3e9d89c11e2b0 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 15:17:00 +0800 Subject: [PATCH 27/38] feat: add application home screen shortcut for unread push notification --- NotificationService/NotificationService.swift | 1 + .../API/Mastodon+API+Notification.swift | 10 +- .../Notification/NotificationHeaderInfo.swift | 4 +- .../Service/NotificationService.swift | 44 ++++++++- .../TwidereUI/Content/PollOptionView.swift | 2 +- TwidereX/Coordinator/SceneCoordinator.swift | 6 +- TwidereX/Info.plist | 23 +++++ .../Provider/DataSourceFacade+LIst.swift | 6 +- TwidereX/Supporting Files/AppDelegate.swift | 6 ++ TwidereX/Supporting Files/SceneDelegate.swift | 99 +++++++++++++++++++ 10 files changed, 190 insertions(+), 11 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 81fca613..5fce6ce7 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -7,6 +7,7 @@ // import os.log +import UIKit import UserNotifications import AppShared import TwidereCommon diff --git a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Notification.swift b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Notification.swift index afcea2eb..3254e34a 100644 --- a/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Notification.swift +++ b/TwidereSDK/Sources/MastodonSDK/API/Mastodon+API+Notification.swift @@ -51,7 +51,15 @@ extension Mastodon.API.Notification { public var includeTypes: [Mastodon.Entity.Notification.NotificationType]? { switch self { case .all: - return nil + return [ + .follow, + .followRequest, + .mention, + .reblog, + .favourite, + .poll, + .status + ] case .mentions: return [.mention, .status] } // end switch diff --git a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift index e3041df5..33fb1120 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Notification/NotificationHeaderInfo.swift @@ -67,7 +67,7 @@ extension NotificationHeaderInfo { case .status: return nil case ._other: - assertionFailure() + // assertionFailure() return nil } } @@ -97,7 +97,7 @@ extension NotificationHeaderInfo { case .status: return nil case ._other: - assertionFailure() + // assertionFailure() return nil } return Meta.convert(from: .mastodon(string: text, emojis: user.emojis.asDictionary)) diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 944f1063..9f09c87e 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -8,11 +8,14 @@ import os.log import UIKit import Combine +import CoreData import CoreDataStack import TwidereCommon final public actor NotificationService { + public static let unreadShortcutItemIdentifier = "com.twidere.TwidereCore.NotificationService.unread-shortcut" + let logger = Logger(subsystem: "NotificationService", category: "NotificationService") var disposeBag = Set() @@ -106,6 +109,44 @@ final public actor NotificationService { } +extension NotificationService { + + public nonisolated func unreadApplicationShortcutItems() async -> [UIApplicationShortcutItem] { + guard let authenticationService = await self.authenticationService else { return [] } + let managedObjectContext = authenticationService.managedObjectContext + return await managedObjectContext.perform{ + var items: [UIApplicationShortcutItem] = [] + for object in authenticationService.authenticationIndexes { + guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } + let _accessToken: String? = { + return authenticationIndex.mastodonAuthentication?.userAccessToken + }() + guard let accessToken = _accessToken else { continue} + + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + guard count > 0 else { continue } + + guard let user = authenticationIndex.user else { continue} + let title = "@\(user.username)" + let subtitle = "\(count) notifications" + + let item = UIApplicationShortcutItem( + type: NotificationService.unreadShortcutItemIdentifier, + localizedTitle: title, + localizedSubtitle: subtitle, + icon: nil, + userInfo: [ + "accessToken": accessToken as NSSecureCoding + ] + ) + items.append(item) + } + return items + } + } + +} + extension NotificationService { public func clearNotificationCountForActiveUser() { @@ -152,8 +193,9 @@ extension NotificationService { extension NotificationService { @MainActor - private func updateApplicationIconBadge(count: Int) { + private func updateApplicationIconBadge(count: Int) async { UIApplication.shared.applicationIconBadgeNumber = count + UIApplication.shared.shortcutItems = await unreadApplicationShortcutItems() } private func requestNotificationPermission() async { diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index 9126c33b..b4a25523 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -48,7 +48,7 @@ public final class PollOptionView: UIView { let textField = DeleteBackwardResponseTextField() textField.font = .systemFont(ofSize: 16, weight: .regular) textField.textColor = .label - textField.text = "Choice" // TODO: i18n + textField.text = "Choice" textField.textAlignment = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight ? .left : .right return textField }() diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index 87ff25d6..0164be02 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -19,9 +19,9 @@ final public class SceneCoordinator { private var disposeBag = Set() - private weak var scene: UIScene! - private weak var sceneDelegate: SceneDelegate! - private weak var context: AppContext! + private(set) weak var scene: UIScene! + private(set) weak var sceneDelegate: SceneDelegate! + private(set) weak var context: AppContext! let id = UUID().uuidString diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index f50c3755..41265019 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -2,6 +2,25 @@ + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconSymbolName + square.and.pencil + UIApplicationShortcutItemTitle + ComposeShortcutItemTitle + UIApplicationShortcutItemType + com.twidere.TwidereX.compose + + + UIApplicationShortcutItemIconSymbolName + magnifyingglass + UIApplicationShortcutItemTitle + SearchShortcutItemTitle + UIApplicationShortcutItemType + com.twidere.TwidereX.search + + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -61,6 +80,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + remote-notification + UILaunchStoryboardName Main UIMainStoryboardFile diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift index 9586e8e2..3720c62f 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+LIst.swift @@ -352,12 +352,12 @@ extension DataSourceFacade { authenticationContext: authenticationContext ) notificationFeedbackGenerator.notificationOccurred(.success) - presentSuccessBanner(title: "List Deleted") // TODO: i18n + presentSuccessBanner(title: L10n.Common.Alerts.ListDeleted.title) } catch { notificationFeedbackGenerator.notificationOccurred(.error) presentWarningBanner( - title: "Failed to Delete List", // TODO: i18n - message: "Please try again", // TODO: i18n + title: L10n.Common.Alerts.FailedToDeleteList.title, + message: L10n.Common.Alerts.FailedToDeleteList.message, error: error ) } diff --git a/TwidereX/Supporting Files/AppDelegate.swift b/TwidereX/Supporting Files/AppDelegate.swift index 7ddc5605..8ef813a9 100644 --- a/TwidereX/Supporting Files/AppDelegate.swift +++ b/TwidereX/Supporting Files/AppDelegate.swift @@ -122,6 +122,12 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound]) } + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { + let shortcutItems = await appContext.notificationService.unreadApplicationShortcutItems() + UIApplication.shared.shortcutItems = shortcutItems + return .noData + } + // response to user action for notification (e.g. redirect to post) func userNotificationCenter( _ center: UNUserNotificationCenter, diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 116767a5..cb1778f0 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -11,6 +11,8 @@ import Combine import Intents import FPSIndicator import CoreDataStack +import TwidereCore +import AppShared class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -133,6 +135,103 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } +} + +extension SceneDelegate { + + func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem + ) async -> Bool { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(shortcutItem.type)") + guard let coordinator = self.coordinator else { return false } + + func topMostViewController() -> UIViewController? { + return coordinator.sceneDelegate.window?.rootViewController?.topMost + } + + + switch shortcutItem.type { + case "com.twidere.TwidereX.new-post": + if let topMost = topMostViewController(), topMost.isModal { + topMost.dismiss(animated: false) + } + let composeViewModel = ComposeViewModel(context: coordinator.context) + let composeContentViewModel = ComposeContentViewModel( + kind: .post, + configurationContext: .init( + apiService: coordinator.context.apiService, + authenticationService: coordinator.context.authenticationService, + mastodonEmojiService: coordinator.context.mastodonEmojiService, + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: coordinator.context.authenticationService.$activeAuthenticationContext + ) + ) + ) + coordinator.present( + scene: .compose( + viewModel: composeViewModel, + contentViewModel: composeContentViewModel + ), + from: nil, + transition: .modal(animated: true) + ) + return true + case "com.twidere.TwidereX.search": + if let topMost = topMostViewController(), topMost.isModal { + topMost.dismiss(animated: false) + } + coordinator.switchToTabBar(tab: .search) + return true + case NotificationService.unreadShortcutItemIdentifier: + if let topMost = topMostViewController(), topMost.isModal { + topMost.dismiss(animated: false) + } + coordinator.switchToTabBar(tab: .notification) + return true + default: + assertionFailure() + return false + } + } + +// private func handler(shortcutItem: UIApplicationShortcutItem) async -> Bool { +// +// switch shortcutItem.type { +// case "org.joinmastodon.app.new-post": +// if coordinator?.tabBarController.topMost is ComposeViewController { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") +// } else { +// if let authenticationBox = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value { +// let composeViewModel = ComposeViewModel( +// context: AppContext.shared, +// composeKind: .post, +// authenticationBox: authenticationBox +// ) +// coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") +// } else { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") +// } +// } +// case "org.joinmastodon.app.search": +// coordinator?.switchToTabBar(tab: .search) +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select search tab") +// +// if let searchViewController = coordinator?.tabBarController.topMost as? SearchViewController { +// searchViewController.searchBarTapPublisher.send() +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search") +// } +// default: +// assertionFailure() +// break +// } +// +// return true +// } + } #if DEBUG From a961c78946d2f71bec98091bb094775a0fb0b2e4 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 15:20:57 +0800 Subject: [PATCH 28/38] chore: update i18n resources --- .../Generated/Strings.swift | 40 +++++ .../Resources/ar.lproj/Localizable.strings | 14 ++ .../ar.lproj/Localizable.stringsdict | 24 +++ .../Resources/ca.lproj/Localizable.strings | 14 ++ .../ca.lproj/Localizable.stringsdict | 16 ++ .../Resources/de.lproj/Localizable.strings | 166 ++++++++++-------- .../de.lproj/Localizable.stringsdict | 16 ++ .../Resources/en.lproj/Localizable.strings | 14 ++ .../en.lproj/Localizable.stringsdict | 16 ++ .../Resources/es.lproj/Localizable.strings | 14 ++ .../es.lproj/Localizable.stringsdict | 16 ++ .../Resources/eu.lproj/Localizable.strings | 14 ++ .../eu.lproj/Localizable.stringsdict | 16 ++ .../Resources/gl.lproj/Localizable.strings | 50 ++++-- .../gl.lproj/Localizable.stringsdict | 16 ++ .../Resources/ja.lproj/Localizable.strings | 80 +++++---- .../ja.lproj/Localizable.stringsdict | 14 ++ .../Resources/ko.lproj/Localizable.strings | 14 ++ .../ko.lproj/Localizable.stringsdict | 32 +++- .../Resources/pt-BR.lproj/Localizable.strings | 24 ++- .../pt-BR.lproj/Localizable.stringsdict | 16 ++ .../Resources/tr.lproj/Localizable.strings | 24 ++- .../tr.lproj/Localizable.stringsdict | 16 ++ .../zh-Hans.lproj/Localizable.strings | 14 ++ .../zh-Hans.lproj/Localizable.stringsdict | 14 ++ TwidereX/Resources/ar.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/ca.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/de.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/en.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/es.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/eu.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/gl.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/ja.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/ko.lproj/InfoPlist.strings | 4 +- .../Resources/pt-BR.lproj/InfoPlist.strings | 4 +- TwidereX/Resources/tr.lproj/InfoPlist.strings | 4 +- .../Resources/zh-Hans.lproj/InfoPlist.strings | 4 +- TwidereXIntent/gl.lproj/Intents.strings | 2 +- TwidereXIntent/ko.lproj/Intents.strings | 40 ++--- 39 files changed, 605 insertions(+), 179 deletions(-) diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 1070d871..106a3766 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -595,6 +595,14 @@ public enum L10n { public static func blockUser(_ p1: Any) -> String { return L10n.tr("Localizable", "Common.Controls.Friendship.BlockUser", String(describing: p1)) } + /// Do you want to report and block %@ + public static func doYouWantToReportAndBlockUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportAnd BlockUser", String(describing: p1)) + } + /// Do you want to report %@ + public static func doYouWantToReportUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Friendship.DoYouWantToReportUser", String(describing: p1)) + } /// follower public static let follower = L10n.tr("Localizable", "Common.Controls.Friendship.Follower") /// followers @@ -1284,6 +1292,18 @@ public enum L10n { public static let backgroundShadow = L10n.tr("Localizable", "Scene.Settings.About.Logo.BackgroundShadow") } } + public enum Account { + /// Account Settings + public static let accountSettings = L10n.tr("Localizable", "Scene.Settings.Account.AccountSettings") + /// Blocked People + public static let blockedPeople = L10n.tr("Localizable", "Scene.Settings.Account.BlockedPeople") + /// Mute and Block + public static let muteAndBlock = L10n.tr("Localizable", "Scene.Settings.Account.MuteAndBlock") + /// Muted People + public static let mutedPeople = L10n.tr("Localizable", "Scene.Settings.Account.MutedPeople") + /// Account + public static let title = L10n.tr("Localizable", "Scene.Settings.Account.Title") + } public enum Appearance { /// AMOLED optimized mode public static let amoledOptimizedMode = L10n.tr("Localizable", "Scene.Settings.Appearance.AmoledOptimizedMode") @@ -1477,12 +1497,28 @@ public enum L10n { public static let accounts = L10n.tr("Localizable", "Scene.Settings.Notification.Accounts") /// Show Notification public static let notificationSwitch = L10n.tr("Localizable", "Scene.Settings.Notification.NotificationSwitch") + /// Push Notification + public static let pushNotification = L10n.tr("Localizable", "Scene.Settings.Notification.PushNotification") /// Notification public static let title = L10n.tr("Localizable", "Scene.Settings.Notification.Title") + public enum Mastodon { + /// Favorite + public static let favorite = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Favorite") + /// Mention + public static let mention = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Mention") + /// New Follow + public static let newFollow = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.NewFollow") + /// poll + public static let poll = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Poll") + /// Reblog + public static let reblog = L10n.tr("Localizable", "Scene.Settings.Notification.Mastodon.Reblog") + } } public enum SectionHeader { /// About public static let about = L10n.tr("Localizable", "Scene.Settings.SectionHeader.About") + /// Account + public static let account = L10n.tr("Localizable", "Scene.Settings.SectionHeader.Account") /// General public static let general = L10n.tr("Localizable", "Scene.Settings.SectionHeader.General") } @@ -1595,6 +1631,10 @@ public enum L10n { public static func media(_ p1: Int) -> String { return L10n.tr("Localizable", "count.media", p1) } + /// Plural format key: "%#@count.notification@" + public static func notification(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.notification", p1) + } /// Plural format key: "%#@count_people@" public static func people(_ p1: Int) -> String { return L10n.tr("Localizable", "count.people", p1) diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index 276b6835..1d635c1c 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "الغ متابعته"; "Common.Controls.Friendship.Actions.Unmute" = "ألغ الكتم"; "Common.Controls.Friendship.BlockUser" = "احجب %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "متابِع"; "Common.Controls.Friendship.Followers" = "متابِعون"; "Common.Controls.Friendship.FollowsYou" = "يتابعك"; @@ -396,6 +398,11 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "تظليل شعار الخلفية لصفحة 'حول'"; "Scene.Settings.About.Title" = "حول"; "Scene.Settings.About.Version" = "النسخة %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "الحساب"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "الوضع المحسّن لـ AMOLED"; "Scene.Settings.Appearance.AppIcon" = "أيقونة التطبيق"; "Scene.Settings.Appearance.HighlightColor" = "لون النص البارز"; @@ -466,9 +473,16 @@ "Scene.Settings.Misc.Proxy.Username" = "اسم المستخدم"; "Scene.Settings.Misc.Title" = "متفرقات"; "Scene.Settings.Notification.Accounts" = "الحسابات"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "أظهر الإشعارات"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "التنبيهات"; "Scene.Settings.SectionHeader.About" = "حول"; +"Scene.Settings.SectionHeader.Account" = "الحساب"; "Scene.Settings.SectionHeader.General" = "عامّ"; "Scene.Settings.Storage.All.SubTitle" = "حذف كل التخزين المؤقت لـ Twidere X. لن تفقد بيانات اعتماد حسابك."; "Scene.Settings.Storage.All.Title" = "امسح التخزين المؤقت"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict index f1fc1d3e..2d7c8180 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict @@ -2,6 +2,30 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld notifications + one + 1 notification + two + %ld notifications + few + %ld notifications + many + %ld notifications + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index db790110..58d211d5 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixa de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Deixar de silenciar"; "Common.Controls.Friendship.BlockUser" = "Bloca %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidors"; "Common.Controls.Friendship.FollowsYou" = "Et segueix"; @@ -396,6 +398,11 @@ Encara en una fase inicial."; "Scene.Settings.About.Logo.BackgroundShadow" = "Quant al ombrejat del logo del fons de la pàgina"; "Scene.Settings.About.Title" = "Quant a"; "Scene.Settings.About.Version" = "Versió %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Account"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimized mode"; "Scene.Settings.Appearance.AppIcon" = "App Icon"; "Scene.Settings.Appearance.HighlightColor" = "Color de realçament"; @@ -466,9 +473,16 @@ Encara en una fase inicial."; "Scene.Settings.Misc.Proxy.Username" = "Username"; "Scene.Settings.Misc.Title" = "Misc"; "Scene.Settings.Notification.Accounts" = "Comptes"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Show Notification"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notificació"; "Scene.Settings.SectionHeader.About" = "Quant a"; +"Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "General"; "Scene.Settings.Storage.All.SubTitle" = "Delete all Twidere X cache. Your account credentials will not be lost."; "Scene.Settings.Storage.All.Title" = "Clear all cache"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict index e41b46c4..7c65d2a5 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index f8601d83..bbdd38bd 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -180,7 +180,7 @@ "Common.Controls.Actions.TakePhoto" = "Foto aufnehmen"; "Common.Controls.Actions.Yes" = "Ja"; "Common.Controls.Friendship.Actions.Block" = "Blockieren"; -"Common.Controls.Friendship.Actions.Blocked" = "Blocked"; +"Common.Controls.Friendship.Actions.Blocked" = "Blockiert"; "Common.Controls.Friendship.Actions.Follow" = "Folgen"; "Common.Controls.Friendship.Actions.Following" = "Folgen"; "Common.Controls.Friendship.Actions.Mute" = "Stumm schalten"; @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Entfolgen"; "Common.Controls.Friendship.Actions.Unmute" = "Stummschaltung aufheben"; "Common.Controls.Friendship.BlockUser" = "%@ blockieren"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "folgender"; "Common.Controls.Friendship.Followers" = "folgende"; "Common.Controls.Friendship.FollowsYou" = "Folgt dir"; @@ -199,7 +201,7 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@ folgt dir"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@ folgt dir nicht"; "Common.Controls.Ios.PhotoLibrary" = "Fo­to­ga­le­rie"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "Keine Ergebnisse"; "Common.Controls.ProfileDashboard.Followers" = "Follower"; "Common.Controls.ProfileDashboard.Following" = "Folgen"; "Common.Controls.ProfileDashboard.Listed" = "Listet"; @@ -224,40 +226,40 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ Personen"; "Common.Controls.Status.Poll.TotalVote" = "%@ Stimmen"; "Common.Controls.Status.Poll.TotalVotes" = "%@ Stimmen"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "Personen die %@ folgen oder erwähnt haben, können antworten."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "Die von %@ erwähnten Personen können antworten."; "Common.Controls.Status.Thread.Show" = "Dieses Thema anzeigen"; "Common.Controls.Status.UserBoosted" = "%@ hat geboostet"; "Common.Controls.Status.UserRetweeted" = "%@ retweeted"; "Common.Controls.Status.YouBoosted" = "Du hast geboostet"; "Common.Controls.Status.YouRetweeted" = "Du hast retweetet"; "Common.Controls.Timeline.LoadMore" = "Mehr laden"; -"Common.Controls.User.Actions.AddRemoveFromLists" = "Add/remove from Lists"; -"Common.Controls.User.Actions.ViewListed" = "View Listed"; +"Common.Controls.User.Actions.AddRemoveFromLists" = "Zu Listen hinzufügen/entfernen"; +"Common.Controls.User.Actions.ViewListed" = "Liste anzeigen"; "Common.Controls.User.Actions.ViewLists" = "Listen anzeigen"; -"Common.Countable.Like.Multiple" = "%@ likes"; -"Common.Countable.Like.Single" = "%@ like"; -"Common.Countable.List.Multiple" = "%@ lists"; -"Common.Countable.List.Single" = "%@ list"; -"Common.Countable.Member.Multiple" = "%@ members"; -"Common.Countable.Member.Single" = "%@ member"; -"Common.Countable.Photo.Multiple" = "%@ photos"; -"Common.Countable.Photo.Single" = "%@ photo"; -"Common.Countable.Quote.Mutiple" = "%@ quotes"; -"Common.Countable.Quote.Single" = "%@ quote"; -"Common.Countable.Reply.Mutiple" = "%@ replies"; -"Common.Countable.Reply.Single" = "%@ reply"; -"Common.Countable.Retweet.Mutiple" = "%@ retweets"; -"Common.Countable.Retweet.Single" = "%@ retweet"; -"Common.Countable.Tweet.Multiple" = "%@ tweets"; -"Common.Countable.Tweet.Single" = "%@ tweet"; +"Common.Countable.Like.Multiple" = "%@ Gefällt mir-Angaben"; +"Common.Countable.Like.Single" = "%@ Gefällt mir"; +"Common.Countable.List.Multiple" = ": %@ Listen"; +"Common.Countable.List.Single" = ": %@ Liste"; +"Common.Countable.Member.Multiple" = "%@ Mitglieder"; +"Common.Countable.Member.Single" = "%@ Mitglied"; +"Common.Countable.Photo.Multiple" = "%@ Fotos"; +"Common.Countable.Photo.Single" = "%@ Foto"; +"Common.Countable.Quote.Mutiple" = "%@ Zitate"; +"Common.Countable.Quote.Single" = "%@ Zitieren"; +"Common.Countable.Reply.Mutiple" = "%@ Antworten"; +"Common.Countable.Reply.Single" = "%@ Antwort"; +"Common.Countable.Retweet.Mutiple" = "%@ Retweets"; +"Common.Countable.Retweet.Single" = "%@ Retweet"; +"Common.Countable.Tweet.Multiple" = "%@ Tweets"; +"Common.Countable.Tweet.Single" = "%@ Tweet"; "Common.Notification.Favourite" = "%@ favorisierte deinen Beitrag"; "Common.Notification.Follow" = "%@ folgt dir"; "Common.Notification.FollowRequest" = "%@ hat angefragt dir zu folgen"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "Bestätigen"; +"Common.Notification.FollowRequestAction.Deny" = "Ablehnen"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follower-Anfrage bestätigt"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follower-Anfrage abgelehnt"; "Common.Notification.Mentions" = "%@ hat dich erwähnt"; "Common.Notification.Messages.Content" = "%@ hat dir einen Nachricht gesendet"; "Common.Notification.Messages.Title" = "Neue Privatnachricht"; @@ -265,10 +267,10 @@ "Common.Notification.Poll" = "Eine Umfrage, an der du teilgenommen hast, ist beendet"; "Common.Notification.Reblog" = "%@ hat deinen Tröt geteilt"; "Common.Notification.Status" = "%@ hat gepostet"; -"Common.NotificationChannel.BackgroundProgresses.Name" = "Background progresses"; -"Common.NotificationChannel.ContentInteractions.Description" = "Interactions like mentions and retweets"; -"Common.NotificationChannel.ContentInteractions.Name" = "Interactions"; -"Common.NotificationChannel.ContentMessages.Description" = "Direct messages"; +"Common.NotificationChannel.BackgroundProgresses.Name" = "Hintergrundprozesse"; +"Common.NotificationChannel.ContentInteractions.Description" = "Interaktionen wie Erwähnungen und Retweets"; +"Common.NotificationChannel.ContentInteractions.Name" = "Interaktionen"; +"Common.NotificationChannel.ContentMessages.Description" = "Direktnachrichten"; "Common.NotificationChannel.ContentMessages.Name" = "Nachrichten"; "Scene.Authentication.Title" = "Authentifizierung"; "Scene.Bookmark.Title" = "Lesezeichen"; @@ -345,32 +347,32 @@ "Scene.ListsDetails.Tabs.Subscriber" = "Subscribers"; "Scene.ListsDetails.Title" = "Listendetails"; "Scene.ListsModify.Create.Title" = "New List"; -"Scene.ListsModify.Description" = "Description"; -"Scene.ListsModify.Dialog.Create" = "Create a list"; -"Scene.ListsModify.Dialog.Edit" = "Rename the list"; +"Scene.ListsModify.Description" = "Beschreibung"; +"Scene.ListsModify.Dialog.Create" = "Eine Liste erstellen"; +"Scene.ListsModify.Dialog.Edit" = "Liste umbenennen"; "Scene.ListsModify.Edit.Title" = "Liste bearbeiten"; "Scene.ListsModify.Name" = "Name"; -"Scene.ListsModify.Private" = "Private"; -"Scene.ListsUsers.Add.Search" = "Search people"; -"Scene.ListsUsers.Add.SearchWithinPeopleYouFollow" = "Search within people you follow"; +"Scene.ListsModify.Private" = "Privat"; +"Scene.ListsUsers.Add.Search" = "Personen suchen"; +"Scene.ListsUsers.Add.SearchWithinPeopleYouFollow" = "Suche innerhalb von Leuten denen du folgst"; "Scene.ListsUsers.Add.Title" = "Mitglied hinzufügen"; "Scene.ListsUsers.MenuActions.Add" = "Hinzufügen"; "Scene.ListsUsers.MenuActions.Remove" = "Entfernen"; -"Scene.Local.Title" = "Local"; +"Scene.Local.Title" = "Lokal"; "Scene.ManageAccounts.DeleteAccount" = "Konto löschen"; "Scene.ManageAccounts.Title" = "Konten"; "Scene.Mentions.Title" = "Erwähnungen"; -"Scene.Messages.Action.CopyText" = "Copy message text"; -"Scene.Messages.Action.Delete" = "Delete message for you"; -"Scene.Messages.Error.NotSupported" = "The Current account does not support direct messages"; +"Scene.Messages.Action.CopyText" = "Nachrichtentext kopieren"; +"Scene.Messages.Action.Delete" = "Nachricht für dich löschen"; +"Scene.Messages.Error.NotSupported" = "Das aktuelle Konto unterstützt keine Direktnachrichten"; "Scene.Messages.Expanded.Photo" = "[Photo]"; -"Scene.Messages.Icon.Failed" = "Send message failed"; -"Scene.Messages.NewConversation.Search" = "Search people"; -"Scene.Messages.NewConversation.Title" = "Find people"; +"Scene.Messages.Icon.Failed" = "Nachricht versenden fehlgeschlagen"; +"Scene.Messages.NewConversation.Search" = "Personen suchen"; +"Scene.Messages.NewConversation.Title" = "Personen finden"; "Scene.Messages.Title" = "Nachrichten"; -"Scene.Notification.Tabs.All" = "All"; +"Scene.Notification.Tabs.All" = "Alle"; "Scene.Notification.Tabs.Mentions" = "Erwähnungen"; -"Scene.Notification.Title" = "Notification"; +"Scene.Notification.Title" = "Benachrichtigung"; "Scene.Profile.Fields.JoinedInDate" = "Joined in %@"; "Scene.Profile.Filter.All" = "All tweets"; "Scene.Profile.Filter.ExcludeReplies" = "Exclude replies"; @@ -384,7 +386,7 @@ "Scene.Search.ShowMore" = "Show more"; "Scene.Search.Tabs.Hashtag" = "Hashtag"; "Scene.Search.Tabs.Media" = "Medien"; -"Scene.Search.Tabs.People" = "People"; +"Scene.Search.Tabs.People" = "Personen"; "Scene.Search.Tabs.Toots" = "Toots"; "Scene.Search.Tabs.Tweets" = "Tweets"; "Scene.Search.Tabs.Users" = "Benutzer"; @@ -396,35 +398,40 @@ Still in early stage."; "Scene.Settings.About.Logo.BackgroundShadow" = "About page background logo shadow"; "Scene.Settings.About.Title" = "Über"; "Scene.Settings.About.Version" = "Ver %@"; -"Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimized mode"; -"Scene.Settings.Appearance.AppIcon" = "App Icon"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Konto"; +"Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimierter Modus"; +"Scene.Settings.Appearance.AppIcon" = "App-Symbol"; "Scene.Settings.Appearance.HighlightColor" = "Farbe hervorheben"; "Scene.Settings.Appearance.PickColor" = "Farbe wählen"; -"Scene.Settings.Appearance.ScrollingTimeline.AppBar" = "Hide app bar when scrolling"; -"Scene.Settings.Appearance.ScrollingTimeline.Fab" = "Hide FAB when scrolling"; -"Scene.Settings.Appearance.ScrollingTimeline.TabBar" = "Hide tab bar when scrolling"; +"Scene.Settings.Appearance.ScrollingTimeline.AppBar" = "Anwendungsleiste beim Scrollen verstecken"; +"Scene.Settings.Appearance.ScrollingTimeline.Fab" = "Tweet erstellen Schaltfläche beim Scrollen verstecken"; +"Scene.Settings.Appearance.ScrollingTimeline.TabBar" = "Titelleiste beim Scrollen verstecken"; "Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "Scrolling Timeline"; "Scene.Settings.Appearance.SectionHeader.TabPosition" = "Tab position"; "Scene.Settings.Appearance.SectionHeader.Theme" = "Design"; -"Scene.Settings.Appearance.SectionHeader.Translation" = "Translation"; +"Scene.Settings.Appearance.SectionHeader.Translation" = "Übersetzung"; "Scene.Settings.Appearance.TabPosition.Bottom" = "Unten"; "Scene.Settings.Appearance.TabPosition.Top" = "Oben"; "Scene.Settings.Appearance.Theme.Auto" = "Automatisch"; -"Scene.Settings.Appearance.Theme.Dark" = "Dark"; -"Scene.Settings.Appearance.Theme.Light" = "Light"; +"Scene.Settings.Appearance.Theme.Dark" = "Dunkel"; +"Scene.Settings.Appearance.Theme.Light" = "Hell"; "Scene.Settings.Appearance.Title" = "Aussehen"; "Scene.Settings.Appearance.Translation.Always" = "Immer"; "Scene.Settings.Appearance.Translation.Auto" = "Automatisch"; "Scene.Settings.Appearance.Translation.Off" = "Aus"; -"Scene.Settings.Appearance.Translation.Service" = "Service"; -"Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Appearance.Translation.Service" = "Dienste"; +"Scene.Settings.Appearance.Translation.TranslateButton" = "Übersetzen-Schaltfläche"; "Scene.Settings.Display.DateFormat.Absolute" = "Absolut"; "Scene.Settings.Display.DateFormat.Relative" = "Relativ"; "Scene.Settings.Display.Media.Always" = "Immer"; "Scene.Settings.Display.Media.AutoPlayback" = "Automatische Wiedergabe"; "Scene.Settings.Display.Media.Automatic" = "Automatisch"; "Scene.Settings.Display.Media.MediaPreviews" = "Medien Vorschau"; -"Scene.Settings.Display.Media.MuteByDefault" = "Mute by default"; +"Scene.Settings.Display.Media.MuteByDefault" = "Standardmäßig stummschalten"; "Scene.Settings.Display.Media.Off" = "Aus"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "Vielen Dank für die Verwendung von @TwidereProject!"; "Scene.Settings.Display.SectionHeader.DateFormat" = "Datumsformat"; @@ -436,12 +443,12 @@ Still in early stage."; "Scene.Settings.Display.Text.RoundedSquare" = "Abgerundetes Quadrat"; "Scene.Settings.Display.Text.UseTheSystemFontSize" = "Die Systemschriftgröße verwenden"; "Scene.Settings.Display.Title" = "Anzeige"; -"Scene.Settings.Display.UrlPreview" = "Url previews"; +"Scene.Settings.Display.UrlPreview" = "Link-Vorschau"; "Scene.Settings.Layout.Actions.Drawer" = "Drawer actions"; "Scene.Settings.Layout.Actions.Tabbar" = "Tabbar actions"; "Scene.Settings.Layout.Desc.Content" = "Choose and arrange up to 5 actions that will appear on the tabbar (The local and federal timelines will only be displayed in Mastodon.)"; -"Scene.Settings.Layout.Desc.Title" = "Custom Layout"; -"Scene.Settings.Layout.Title" = "Layout"; +"Scene.Settings.Layout.Desc.Title" = "Benutzerdefinierte Darstellung"; +"Scene.Settings.Layout.Title" = "Darstellung"; "Scene.Settings.Misc.Nitter.Dialog.Information.Content" = "Due to the limitation of Twitter API, some data might not be able to fetch from Twitter, you can use a third-party data provider to provide these data. Twidere does not take any responsibility for them."; "Scene.Settings.Misc.Nitter.Dialog.Information.Title" = "Third party Twitter data provider"; "Scene.Settings.Misc.Nitter.Dialog.Usage.Content" = "- Twitter status threading"; @@ -449,9 +456,9 @@ Still in early stage."; "Scene.Settings.Misc.Nitter.Dialog.Usage.Title" = "Using Third-party data provider in"; "Scene.Settings.Misc.Nitter.Input.Description" = "Alternative Twitter front-end focused on privacy."; "Scene.Settings.Misc.Nitter.Input.Invalid" = "Nitter instance URL is invalid, e.g. https://nitter.net"; -"Scene.Settings.Misc.Nitter.Input.Placeholder" = "Nitter Instance"; +"Scene.Settings.Misc.Nitter.Input.Placeholder" = "Nitter-Instanz"; "Scene.Settings.Misc.Nitter.Input.Value" = "Instance URL"; -"Scene.Settings.Misc.Nitter.Title" = "Third-party Twitter data provider"; +"Scene.Settings.Misc.Nitter.Title" = "Drittanbieter Bereitstellung von Twitter Daten"; "Scene.Settings.Misc.Proxy.Enable.Description" = "Use proxy for all network requests"; "Scene.Settings.Misc.Proxy.Enable.Title" = "Proxy"; "Scene.Settings.Misc.Proxy.Password" = "Password"; @@ -464,18 +471,25 @@ Still in early stage."; "Scene.Settings.Misc.Proxy.Type.Socks" = "SOCKS"; "Scene.Settings.Misc.Proxy.Type.Title" = "Proxy type"; "Scene.Settings.Misc.Proxy.Username" = "Username"; -"Scene.Settings.Misc.Title" = "Misc"; +"Scene.Settings.Misc.Title" = "Sonstiges"; "Scene.Settings.Notification.Accounts" = "Konten"; -"Scene.Settings.Notification.NotificationSwitch" = "Show Notification"; -"Scene.Settings.Notification.Title" = "Notification"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; +"Scene.Settings.Notification.NotificationSwitch" = "Benachrichtigung anzeigen"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; +"Scene.Settings.Notification.Title" = "Benachrichtigung"; "Scene.Settings.SectionHeader.About" = "Über"; +"Scene.Settings.SectionHeader.Account" = "Konto"; "Scene.Settings.SectionHeader.General" = "Allgemein"; -"Scene.Settings.Storage.All.SubTitle" = "Delete all Twidere X cache. Your account credentials will not be lost."; -"Scene.Settings.Storage.All.Title" = "Clear all cache"; -"Scene.Settings.Storage.Media.SubTitle" = "Clear stored media cache."; -"Scene.Settings.Storage.Media.Title" = "Clear media cache"; -"Scene.Settings.Storage.Search.Title" = "Clear search history"; -"Scene.Settings.Storage.Title" = "Storage"; +"Scene.Settings.Storage.All.SubTitle" = "Löscht den gesamten Twidere X Zwischenspeicher. Deine Logindaten bleiben erhalten."; +"Scene.Settings.Storage.All.Title" = "Gesamten Zwischenspeicher leeren"; +"Scene.Settings.Storage.Media.SubTitle" = "Zwischengespeicherte Medien entfernen."; +"Scene.Settings.Storage.Media.Title" = "Medien Zwischenspeicher leeren"; +"Scene.Settings.Storage.Search.Title" = "Suchverlauf löschen"; +"Scene.Settings.Storage.Title" = "Speicher"; "Scene.Settings.Title" = "Einstellungen"; "Scene.SignIn.HelloSignInToGetStarted" = "Hallo! Melden Sie sich an, um loszulegen."; @@ -494,9 +508,9 @@ Melden Sie sich an, um loszulegen."; "Scene.Status.Title" = "Tweet"; "Scene.Status.TitleMastodon" = "Toot"; "Scene.Timeline.Title" = "Timeline"; -"Scene.Trends.Accounts" = "%d people talking"; -"Scene.Trends.Now" = "Trending Now"; +"Scene.Trends.Accounts" = "%d Leute reden"; +"Scene.Trends.Now" = "In den Trends"; "Scene.Trends.Title" = "Trends"; -"Scene.Trends.TrendsLocation" = "Trends Location"; -"Scene.Trends.WorldWide" = "Trends - Worldwide"; -"Scene.Trends.WorldWideWithoutPrefix" = "Worldwide"; \ No newline at end of file +"Scene.Trends.TrendsLocation" = "Trends-Standort"; +"Scene.Trends.WorldWide" = "Trends - Weltweit"; +"Scene.Trends.WorldWideWithoutPrefix" = "Weltweit"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict index 05f45d8b..b55a4b7c 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index f3f70aa4..fc3fa357 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Unfollow"; "Common.Controls.Friendship.Actions.Unmute" = "Unmute"; "Common.Controls.Friendship.BlockUser" = "Block %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "follower"; "Common.Controls.Friendship.Followers" = "followers"; "Common.Controls.Friendship.FollowsYou" = "Follows you"; @@ -396,6 +398,11 @@ Still in early stage."; "Scene.Settings.About.Logo.BackgroundShadow" = "About page background logo shadow"; "Scene.Settings.About.Title" = "About"; "Scene.Settings.About.Version" = "Ver %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Account"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimized mode"; "Scene.Settings.Appearance.AppIcon" = "App Icon"; "Scene.Settings.Appearance.HighlightColor" = "Highlight color"; @@ -466,9 +473,16 @@ Still in early stage."; "Scene.Settings.Misc.Proxy.Username" = "Username"; "Scene.Settings.Misc.Title" = "Misc"; "Scene.Settings.Notification.Accounts" = "Accounts"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Show Notification"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notification"; "Scene.Settings.SectionHeader.About" = "About"; +"Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "General"; "Scene.Settings.Storage.All.SubTitle" = "Delete all Twidere X cache. Your account credentials will not be lost."; "Scene.Settings.Storage.All.Title" = "Clear all cache"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict index e41b46c4..7c65d2a5 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 9a7ff1db..7073335d 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Dejar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "No Silenciar"; "Common.Controls.Friendship.BlockUser" = "Bloquear a %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidores"; "Common.Controls.Friendship.FollowsYou" = "Te sigue"; @@ -396,6 +398,11 @@ Todavía en fase temprana."; "Scene.Settings.About.Logo.BackgroundShadow" = "Acerca de la sombra del logo del fondo de la página"; "Scene.Settings.About.Title" = "Acerca de"; "Scene.Settings.About.Version" = "Ver %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Cuenta"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "Modo optimizado para AMOLED"; "Scene.Settings.Appearance.AppIcon" = "Icono de la aplicación"; "Scene.Settings.Appearance.HighlightColor" = "Color destacado"; @@ -466,9 +473,16 @@ Todavía en fase temprana."; "Scene.Settings.Misc.Proxy.Username" = "Nombre de usuario"; "Scene.Settings.Misc.Title" = "Varios"; "Scene.Settings.Notification.Accounts" = "Cuentas"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Mostrar notificación"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notificación"; "Scene.Settings.SectionHeader.About" = "Acerca de"; +"Scene.Settings.SectionHeader.Account" = "Cuenta"; "Scene.Settings.SectionHeader.General" = "General"; "Scene.Settings.Storage.All.SubTitle" = "Borrar toda la caché de Twidere X. Las credenciales de tu cuenta no se perderán."; "Scene.Settings.Storage.All.Title" = "Limpiar toda la caché"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict index 6362107e..75bdbf87 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings index 4ac47076..5437d511 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Utzi jarraitzeari"; "Common.Controls.Friendship.Actions.Unmute" = "Desmututu"; "Common.Controls.Friendship.BlockUser" = "%@ blokeatu"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "jarratzaile"; "Common.Controls.Friendship.Followers" = "jarratzaileak"; "Common.Controls.Friendship.FollowsYou" = "Jarraitzen zaitu"; @@ -396,6 +398,11 @@ Oraindik hasierako fasean dago."; "Scene.Settings.About.Logo.BackgroundShadow" = "Orrialdearen hondoko logotipoaren itzalari buruz"; "Scene.Settings.About.Title" = "Honi buruz"; "Scene.Settings.About.Version" = "%@ ikusi"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Account"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED modu optimizatua"; "Scene.Settings.Appearance.AppIcon" = "App Icon"; "Scene.Settings.Appearance.HighlightColor" = "Nabarmentzeko kolorea"; @@ -466,9 +473,16 @@ Oraindik hasierako fasean dago."; "Scene.Settings.Misc.Proxy.Username" = "Erabiltzailearen izena"; "Scene.Settings.Misc.Title" = "Hainbat"; "Scene.Settings.Notification.Accounts" = "Kontuak"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Erakutsi jakinarazpena"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Jakinarazpena"; "Scene.Settings.SectionHeader.About" = "Honi buruz"; +"Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "Orokorra"; "Scene.Settings.Storage.All.SubTitle" = "Ezabatu Twidere X katxea. Zure kontuko kredentzialak ez dira galduko."; "Scene.Settings.Storage.All.Title" = "Garbitu cache guztiak"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict index f0013947..09c127db 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings index 1b66559f..101effd7 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.strings @@ -127,8 +127,8 @@ "Common.Alerts.PhotoSaveFail.Message" = "Téntao de novo"; "Common.Alerts.PhotoSaveFail.Title" = "Fallou gardar a foto"; "Common.Alerts.PhotoSaved.Title" = "Foto gardada"; -"Common.Alerts.PostFailInvalidPoll.Message" = "Poll has empty field. Please fulfill the field then try again"; -"Common.Alerts.PostFailInvalidPoll.Title" = "Failed to Publish"; +"Common.Alerts.PostFailInvalidPoll.Message" = "A enquisa ten un campo baleiro. Éncheo e tenta de novo"; +"Common.Alerts.PostFailInvalidPoll.Title" = "Fallou a publicación"; "Common.Alerts.RateLimitExceeded.Message" = "Acadado límite do uso da API de Twitter"; "Common.Alerts.RateLimitExceeded.Title" = "Taxa de uso superada"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "Denunciaches e bloqueaches a %@ por spam"; @@ -158,7 +158,7 @@ "Common.Controls.Actions.Add" = "Engadir"; "Common.Controls.Actions.Browse" = "Navegar"; "Common.Controls.Actions.Cancel" = "Cancelar"; -"Common.Controls.Actions.Clear" = "Clear"; +"Common.Controls.Actions.Clear" = "Limpar"; "Common.Controls.Actions.Confirm" = "Confirmar"; "Common.Controls.Actions.Copy" = "Copiar"; "Common.Controls.Actions.Delete" = "Borrar"; @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Non acalar"; "Common.Controls.Friendship.BlockUser" = "Bloquear a %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "seguidora"; "Common.Controls.Friendship.Followers" = "seguidores"; "Common.Controls.Friendship.FollowsYou" = "Séguete"; @@ -224,8 +226,8 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ persoa"; "Common.Controls.Status.Poll.TotalVote" = "%@ voto"; "Common.Controls.Status.Poll.TotalVotes" = "%@ votos"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "%@ persoas que seguen ou mencionadas poden responder."; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "%@ persoas mencionadas poden responder."; "Common.Controls.Status.Thread.Show" = "Amosar este fío"; "Common.Controls.Status.UserBoosted" = "%@ promovidos"; "Common.Controls.Status.UserRetweeted" = "%@ rechouchíos"; @@ -254,10 +256,10 @@ "Common.Notification.Favourite" = "%@ marcou favorito o teu toot"; "Common.Notification.Follow" = "%@ séguete"; "Common.Notification.FollowRequest" = "%@ pediu seguirte"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "Aprobar"; +"Common.Notification.FollowRequestAction.Deny" = "Rexeitar"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Petición de seguimento aceptada"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Petición de seguimento rexeitada"; "Common.Notification.Mentions" = "%@ mencionoute"; "Common.Notification.Messages.Content" = "%@ enviouche unha mensaxe"; "Common.Notification.Messages.Title" = "Nova mensaxe directa"; @@ -275,17 +277,17 @@ "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "Escribe aquí o teu aviso"; "Scene.Compose.LastEnd" = " e "; -"Scene.Compose.Media.Caption.Add" = "Add Caption"; -"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Add a description for this image"; -"Scene.Compose.Media.Caption.Remove" = "Remove Caption"; -"Scene.Compose.Media.Caption.Update" = "Update Caption"; +"Scene.Compose.Media.Caption.Add" = "Engadir lenda"; +"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Engade unha descrición para esta imaxe"; +"Scene.Compose.Media.Caption.Remove" = "Borrar lenda"; +"Scene.Compose.Media.Caption.Update" = "Actualizar lenda"; "Scene.Compose.Media.Preview" = "Vista previa"; "Scene.Compose.Media.Remove" = "Eliminar"; "Scene.Compose.OthersInThisConversation" = "Máis persoas nesta conversa:"; "Scene.Compose.Placeholder" = "Que está a pasar?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; -"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; -"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "Todos poden responder"; +"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Só as persoas que segues poden responder"; +"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "As persoas que segues poden responder"; "Scene.Compose.ReplyTo" = "Responder a …"; "Scene.Compose.ReplyingTo" = "En resposta a"; "Scene.Compose.SaveDraft.Action" = "Gardar borrador"; @@ -396,6 +398,11 @@ Aínda en fase previa."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sobre a sombra do logo de fondo da páxina"; "Scene.Settings.About.Title" = "Sobre"; "Scene.Settings.About.Version" = "Ver %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Conta"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "Modo otimizado para AMOLED"; "Scene.Settings.Appearance.AppIcon" = "Icona da app"; "Scene.Settings.Appearance.HighlightColor" = "Cor de realce"; @@ -466,9 +473,16 @@ Aínda en fase previa."; "Scene.Settings.Misc.Proxy.Username" = "Nome de usuaria"; "Scene.Settings.Misc.Title" = "Miscelânea"; "Scene.Settings.Notification.Accounts" = "Contas"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Amosar notificación"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notificación"; "Scene.Settings.SectionHeader.About" = "Sobre"; +"Scene.Settings.SectionHeader.Account" = "Conta"; "Scene.Settings.SectionHeader.General" = "Xeral"; "Scene.Settings.Storage.All.SubTitle" = "Eliminar toda a caché do Twidere X. As credenciais da túa conta non se perden."; "Scene.Settings.Storage.All.Title" = "Limpar toda a caché"; @@ -496,6 +510,6 @@ Aínda en fase previa."; "Scene.Trends.Accounts" = "%d persoas falando"; "Scene.Trends.Now" = "Tendencia agora"; "Scene.Trends.Title" = "Tendencias"; -"Scene.Trends.TrendsLocation" = "Trends Location"; +"Scene.Trends.TrendsLocation" = "Localización das tendencias"; "Scene.Trends.WorldWide" = "Tendencias - Mundial"; -"Scene.Trends.WorldWideWithoutPrefix" = "Worldwide"; \ No newline at end of file +"Scene.Trends.WorldWideWithoutPrefix" = "Global"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict index 68cd2c0c..f73ac52c 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 5fd9691f..3c8699fb 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -32,7 +32,7 @@ "Accessibility.Scene.Compose.Location.Enable" = "位置情報を有効化"; "Accessibility.Scene.Compose.MediaInsert.Camera" = "写真を撮る"; "Accessibility.Scene.Compose.MediaInsert.Gif" = "GIFを追加"; -"Accessibility.Scene.Compose.MediaInsert.Library" = "Browse Library"; +"Accessibility.Scene.Compose.MediaInsert.Library" = "ライブラリを参照"; "Accessibility.Scene.Compose.MediaInsert.RecordVideo" = "動画を撮る"; "Accessibility.Scene.Compose.Send" = "送信"; "Accessibility.Scene.Compose.Thread" = "スレッドモード"; @@ -57,7 +57,7 @@ "Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "ダブルタップして長押ししてメニューを表示する"; "Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "ダブルタップしてホールドしてアカウントパネルを開く"; "Accessibility.VoiceOver.DoubleTapToOpenProfile" = "ダブルタップしてプロフィールを開く"; -"Accessibility.VoiceOver.Selected" = "Selected"; +"Accessibility.VoiceOver.Selected" = "選択済み"; "Common.Alerts.AccountSuspended.Message" = "Twitterは%@に違反したアカウントを凍結します"; "Common.Alerts.AccountSuspended.Title" = "アカウントは凍結されています"; "Common.Alerts.AccountSuspended.TwitterRules" = "Twitterルール"; @@ -121,14 +121,14 @@ "Common.Alerts.PermissionDeniedFriendshipBlocked.Title" = "権限がありません"; "Common.Alerts.PermissionDeniedNotAuthorized.Message" = "権限がありません"; "Common.Alerts.PermissionDeniedNotAuthorized.Title" = "権限がありません"; -"Common.Alerts.PhotoCopied.Title" = "Photo Copied"; +"Common.Alerts.PhotoCopied.Title" = "写真がコピーされました"; "Common.Alerts.PhotoCopyFail.Message" = "もう一度やり直してください"; -"Common.Alerts.PhotoCopyFail.Title" = "Failed to Copy Photo"; +"Common.Alerts.PhotoCopyFail.Title" = "写真のコピーに失敗しました"; "Common.Alerts.PhotoSaveFail.Message" = "もう一度やり直してください"; "Common.Alerts.PhotoSaveFail.Title" = "写真の保存に失敗しました"; "Common.Alerts.PhotoSaved.Title" = "写真を保存しました"; -"Common.Alerts.PostFailInvalidPoll.Message" = "Poll has empty field. Please fulfill the field then try again"; -"Common.Alerts.PostFailInvalidPoll.Title" = "Failed to Publish"; +"Common.Alerts.PostFailInvalidPoll.Message" = "投票に空欄のフィールドが存在します。空欄を埋めてやり直してください。"; +"Common.Alerts.PostFailInvalidPoll.Title" = "公開に失敗しました"; "Common.Alerts.RateLimitExceeded.Message" = "Twitter APIの使用制限に達しました"; "Common.Alerts.RateLimitExceeded.Title" = "レート制限を超えました"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ はスパムとして報告されブロックされました"; @@ -158,9 +158,9 @@ "Common.Controls.Actions.Add" = "追加"; "Common.Controls.Actions.Browse" = "参照"; "Common.Controls.Actions.Cancel" = "キャンセル"; -"Common.Controls.Actions.Clear" = "Clear"; +"Common.Controls.Actions.Clear" = "削除"; "Common.Controls.Actions.Confirm" = "確認"; -"Common.Controls.Actions.Copy" = "Copy"; +"Common.Controls.Actions.Copy" = "コピー"; "Common.Controls.Actions.Delete" = "削除"; "Common.Controls.Actions.Done" = "完了"; "Common.Controls.Actions.Edit" = "編集"; @@ -173,7 +173,7 @@ "Common.Controls.Actions.Share" = "共有"; "Common.Controls.Actions.ShareLink" = "リンクを共有"; "Common.Controls.Actions.ShareMedia" = "メディアを共有"; -"Common.Controls.Actions.ShareMediaMenu.Link" = "Link"; +"Common.Controls.Actions.ShareMediaMenu.Link" = "リンク"; "Common.Controls.Actions.ShareMediaMenu.Media" = "メディア"; "Common.Controls.Actions.SignIn" = "サインイン"; "Common.Controls.Actions.SignOut" = "サインアウト"; @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "フォロー解除"; "Common.Controls.Friendship.Actions.Unmute" = "ミュート解除"; "Common.Controls.Friendship.BlockUser" = "%@ をブロック"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "フォロワー"; "Common.Controls.Friendship.Followers" = "フォロワー"; "Common.Controls.Friendship.FollowsYou" = "あなたをフォローしています"; @@ -211,7 +213,7 @@ "Common.Controls.Status.Actions.PinOnProfile" = "プロフィールに固定表示"; "Common.Controls.Status.Actions.Quote" = "引用"; "Common.Controls.Status.Actions.Retweet" = "リツイート"; -"Common.Controls.Status.Actions.SaveMedia" = "Save media"; +"Common.Controls.Status.Actions.SaveMedia" = "メディアを保存"; "Common.Controls.Status.Actions.Share" = "共有"; "Common.Controls.Status.Actions.ShareContent" = "コンテンツを共有"; "Common.Controls.Status.Actions.ShareLink" = "リンクを共有"; @@ -224,8 +226,8 @@ "Common.Controls.Status.Poll.TotalPerson" = "%@ 人"; "Common.Controls.Status.Poll.TotalVote" = "%@ 票"; "Common.Controls.Status.Poll.TotalVotes" = "%@ 票"; -"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "People %@ follows or mentioned can reply."; -"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "People %@ mentioned can reply."; +"Common.Controls.Status.ReplySettings.PeopleUserFollowsOrMentionedCanReply" = "%@ がフォローまたはメンションした人は返信できます。"; +"Common.Controls.Status.ReplySettings.PeopleUserMentionedCanReply" = "%@ さんが返信できます。"; "Common.Controls.Status.Thread.Show" = "このスレッドを表示"; "Common.Controls.Status.UserBoosted" = "%@ がブーストしました"; "Common.Controls.Status.UserRetweeted" = "%@ がリツイート"; @@ -254,10 +256,10 @@ "Common.Notification.Favourite" = "%@ さんがトゥートをお気に入りにしました"; "Common.Notification.Follow" = "%@ がフォローしました"; "Common.Notification.FollowRequest" = "%@ がフォローリクエストしました"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "承認"; +"Common.Notification.FollowRequestAction.Deny" = "拒否"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "フォローリクエストが承認されました"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "フォローリクエストが拒否されました"; "Common.Notification.Mentions" = "%@ がメンションしました"; "Common.Notification.Messages.Content" = "%@ がメッセージを送信しました"; "Common.Notification.Messages.Title" = "新しいダイレクト メッセージ"; @@ -275,17 +277,17 @@ "Scene.Compose.And" = ", "; "Scene.Compose.CwPlaceholder" = "ここに警告を書いてください"; "Scene.Compose.LastEnd" = " と "; -"Scene.Compose.Media.Caption.Add" = "Add Caption"; -"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "Add a description for this image"; -"Scene.Compose.Media.Caption.Remove" = "Remove Caption"; -"Scene.Compose.Media.Caption.Update" = "Update Caption"; +"Scene.Compose.Media.Caption.Add" = "キャプションを追加"; +"Scene.Compose.Media.Caption.AddADescriptionForThisImage" = "画像の説明を追加する"; +"Scene.Compose.Media.Caption.Remove" = "キャプションを削除"; +"Scene.Compose.Media.Caption.Update" = "キャプションを更新"; "Scene.Compose.Media.Preview" = "プレビュー"; "Scene.Compose.Media.Remove" = "削除"; "Scene.Compose.OthersInThisConversation" = "この会話の他の人:"; "Scene.Compose.Placeholder" = "いまどうしてる?"; -"Scene.Compose.ReplySettings.EveryoneCanReply" = "Everyone can peply"; -"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "Only people you mention can reply"; -"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "People you follow can reply"; +"Scene.Compose.ReplySettings.EveryoneCanReply" = "誰でもリプライすることができます。"; +"Scene.Compose.ReplySettings.OnlyPeopleYouMentionCanReply" = "返信できるのはあなたがメンションした人のみです"; +"Scene.Compose.ReplySettings.PeopleYouFollowCanReply" = "フォローしている人が返信できます"; "Scene.Compose.ReplyTo" = "返信 ..."; "Scene.Compose.ReplyingTo" = "返信先"; "Scene.Compose.SaveDraft.Action" = "下書きを保存"; @@ -309,7 +311,7 @@ "Scene.Compose.Vote.Expiration.6Hour" = "6 時間"; "Scene.Compose.Vote.Expiration.7Day" = "7 日"; "Scene.Compose.Vote.Multiple" = "複数選択"; -"Scene.Compose.Vote.PlaceholderIndex" = "Choice %d"; +"Scene.Compose.Vote.PlaceholderIndex" = "選択肢 %d"; "Scene.ComposeHashtagSearch.SearchPlaceholder" = "ハッシュタグを検索"; "Scene.ComposeUserSearch.SearchPlaceholder" = "ユーザーを検索"; "Scene.Drafts.Actions.DeleteDraft" = "下書きを削除"; @@ -323,7 +325,7 @@ "Scene.Likes.Title" = "いいね"; "Scene.Listed.Title" = "リスト"; "Scene.Lists.Icons.Create" = "リストを作成"; -"Scene.Lists.Icons.Private" = "Private visibility"; +"Scene.Lists.Icons.Private" = "プライベート表示"; "Scene.Lists.Tabs.Created" = "自分のリスト"; "Scene.Lists.Tabs.Subscribed" = "フォロー済み"; "Scene.Lists.Title" = "リスト"; @@ -384,7 +386,7 @@ "Scene.Search.ShowMore" = "もっと表示"; "Scene.Search.Tabs.Hashtag" = "ハッシュタグ"; "Scene.Search.Tabs.Media" = "メディア"; -"Scene.Search.Tabs.People" = "People"; +"Scene.Search.Tabs.People" = "人々"; "Scene.Search.Tabs.Toots" = "トゥート"; "Scene.Search.Tabs.Tweets" = "ツイート"; "Scene.Search.Tabs.Users" = "ユーザー"; @@ -396,8 +398,13 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "情報ページの背景のロゴの影"; "Scene.Settings.About.Title" = "情報"; "Scene.Settings.About.Version" = "Ver %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Account"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED 最適化モード"; -"Scene.Settings.Appearance.AppIcon" = "App Icon"; +"Scene.Settings.Appearance.AppIcon" = "アプリアイコン"; "Scene.Settings.Appearance.HighlightColor" = "ハイライト色"; "Scene.Settings.Appearance.PickColor" = "色を選択"; "Scene.Settings.Appearance.ScrollingTimeline.AppBar" = "スクロール時にアプリバーを隠す"; @@ -406,7 +413,7 @@ "Scene.Settings.Appearance.SectionHeader.ScrollingTimeline" = "タイムラインのスクロール"; "Scene.Settings.Appearance.SectionHeader.TabPosition" = "タブの位置"; "Scene.Settings.Appearance.SectionHeader.Theme" = "テーマ"; -"Scene.Settings.Appearance.SectionHeader.Translation" = "Translation"; +"Scene.Settings.Appearance.SectionHeader.Translation" = "翻訳"; "Scene.Settings.Appearance.TabPosition.Bottom" = "下"; "Scene.Settings.Appearance.TabPosition.Top" = "上"; "Scene.Settings.Appearance.Theme.Auto" = "自動"; @@ -416,8 +423,8 @@ "Scene.Settings.Appearance.Translation.Always" = "常に"; "Scene.Settings.Appearance.Translation.Auto" = "自動"; "Scene.Settings.Appearance.Translation.Off" = "オフ"; -"Scene.Settings.Appearance.Translation.Service" = "Service"; -"Scene.Settings.Appearance.Translation.TranslateButton" = "Translate button"; +"Scene.Settings.Appearance.Translation.Service" = "サービス"; +"Scene.Settings.Appearance.Translation.TranslateButton" = "翻訳ボタン"; "Scene.Settings.Display.DateFormat.Absolute" = "絶対"; "Scene.Settings.Display.DateFormat.Relative" = "相対"; "Scene.Settings.Display.Media.Always" = "常に"; @@ -467,9 +474,16 @@ "Scene.Settings.Misc.Proxy.Username" = "ユーザー名"; "Scene.Settings.Misc.Title" = "その他"; "Scene.Settings.Notification.Accounts" = "アカウント"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "通知を表示"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "通知"; "Scene.Settings.SectionHeader.About" = "情報"; +"Scene.Settings.SectionHeader.Account" = "Account"; "Scene.Settings.SectionHeader.General" = "一般"; "Scene.Settings.Storage.All.SubTitle" = "すべての Twidere X のキャッシュを削除します。アカウントの情報は失われません。"; "Scene.Settings.Storage.All.Title" = "すべてのキャッシュを削除"; @@ -495,9 +509,9 @@ "Scene.Status.Title" = "ツイート"; "Scene.Status.TitleMastodon" = "トゥート"; "Scene.Timeline.Title" = "タイムライン"; -"Scene.Trends.Accounts" = "%d people talking"; +"Scene.Trends.Accounts" = "%d人がこの話題について話しています"; "Scene.Trends.Now" = "トレンド"; "Scene.Trends.Title" = "トレンド"; -"Scene.Trends.TrendsLocation" = "Trends Location"; +"Scene.Trends.TrendsLocation" = "トレンド ロケーション"; "Scene.Trends.WorldWide" = "トレンド - 全世界"; -"Scene.Trends.WorldWideWithoutPrefix" = "Worldwide"; \ No newline at end of file +"Scene.Trends.WorldWideWithoutPrefix" = "世界中"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict index a3c45b09..16cfe3c8 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict @@ -2,6 +2,20 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 9c6268ca..3d835bb7 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "팔로우 끊기"; "Common.Controls.Friendship.Actions.Unmute" = "숨기기 풀기"; "Common.Controls.Friendship.BlockUser" = "%@ 차단하기"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "팔로워"; "Common.Controls.Friendship.Followers" = "팔로워"; "Common.Controls.Friendship.FollowsYou" = "나를 팔로우하고 있음"; @@ -396,6 +398,11 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "페이지 바탕 로고의 그림자에 대하여"; "Scene.Settings.About.Title" = "정보"; "Scene.Settings.About.Version" = "%@ 버전"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "계정"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED 최적화 모드"; "Scene.Settings.Appearance.AppIcon" = "App Icon"; "Scene.Settings.Appearance.HighlightColor" = "강조색"; @@ -466,9 +473,16 @@ "Scene.Settings.Misc.Proxy.Username" = "사용자 이름"; "Scene.Settings.Misc.Title" = "잡동사니"; "Scene.Settings.Notification.Accounts" = "계정"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "알림 보이기"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "알림"; "Scene.Settings.SectionHeader.About" = "정보"; +"Scene.Settings.SectionHeader.Account" = "계정"; "Scene.Settings.SectionHeader.General" = "일반"; "Scene.Settings.Storage.All.SubTitle" = "트위데레X의 모든 캐시를 지웁니다. 계정은 지워지지 않습니다."; "Scene.Settings.Storage.All.Title" = "모든 캐시 지우기"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict index 23182c92..0e94be8e 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict @@ -2,6 +2,20 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld notifications + + count.media NSStringLocalizedFormatKey @@ -13,7 +27,7 @@ NSStringFormatValueTypeKey ld other - %ld media + %ld개의 미디어 count.input_limit_remains @@ -27,7 +41,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + 남은 글자 수: %ld count.metric_formatted.post @@ -41,7 +55,7 @@ NSStringFormatValueTypeKey ld other - posts + 게시글 count.post @@ -55,7 +69,7 @@ NSStringFormatValueTypeKey ld other - %ld posts + %ld개의 게시물 count.quote @@ -69,7 +83,7 @@ NSStringFormatValueTypeKey ld other - %ld quotes + %ld개의 인용 count.like @@ -83,7 +97,7 @@ NSStringFormatValueTypeKey ld other - %ld likes + %ld개의 마음에 들어요 count.retweet @@ -97,7 +111,7 @@ NSStringFormatValueTypeKey ld other - %ld retweets + %ld개의 리트윗 count.reblog @@ -111,7 +125,7 @@ NSStringFormatValueTypeKey ld other - %ld reblogs + %ld개의 리블로그 count.reply @@ -125,7 +139,7 @@ NSStringFormatValueTypeKey ld other - %ld replies + %ld개의 답글 count.vote diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index 6dcfcc27..caba44f9 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Deixar de seguir"; "Common.Controls.Friendship.Actions.Unmute" = "Dessilenciar"; "Common.Controls.Friendship.BlockUser" = "Bloquear %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "seguidor"; "Common.Controls.Friendship.Followers" = "seguidores"; "Common.Controls.Friendship.FollowsYou" = "Segue você"; @@ -254,10 +256,10 @@ "Common.Notification.Favourite" = "%@ favoritou seu toot"; "Common.Notification.Follow" = "%@ seguiu você"; "Common.Notification.FollowRequest" = "%@ pediu para segui-lo"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "Aprovar"; +"Common.Notification.FollowRequestAction.Deny" = "Negar"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Solicitação Para Seguir Aprovada"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Solicitação Para Seguir Negada"; "Common.Notification.Mentions" = "%@ mencionou você"; "Common.Notification.Messages.Content" = "%@ te enviou uma mensagem"; "Common.Notification.Messages.Title" = "Nova mensagem direta"; @@ -396,6 +398,11 @@ Ainda na fase inicial."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sobre a sombra do logo da página de fundo"; "Scene.Settings.About.Title" = "Sobre"; "Scene.Settings.About.Version" = "Ver %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Conta"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "Modo otimizado para AMOLED"; "Scene.Settings.Appearance.AppIcon" = "Ícone do Aplicativo"; "Scene.Settings.Appearance.HighlightColor" = "Cor de destaque"; @@ -466,9 +473,16 @@ Ainda na fase inicial."; "Scene.Settings.Misc.Proxy.Username" = "Nome de usuário"; "Scene.Settings.Misc.Title" = "Miscelânea"; "Scene.Settings.Notification.Accounts" = "Contas"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Mostrar Notificação"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Notificação"; "Scene.Settings.SectionHeader.About" = "Sobre"; +"Scene.Settings.SectionHeader.Account" = "Conta"; "Scene.Settings.SectionHeader.General" = "Geral"; "Scene.Settings.Storage.All.SubTitle" = "Exclua todo o cache do Twidere X. Suas credenciais de conta não serão perdidas."; "Scene.Settings.Storage.All.Title" = "Limpar todo o cache"; @@ -497,6 +511,6 @@ Faça Login para Começar."; "Scene.Trends.Accounts" = "%d pessoas falando"; "Scene.Trends.Now" = "Tendências Agora"; "Scene.Trends.Title" = "Tendências"; -"Scene.Trends.TrendsLocation" = "Trends Location"; +"Scene.Trends.TrendsLocation" = "Localização Das Tendências"; "Scene.Trends.WorldWide" = "Tendências - Global"; "Scene.Trends.WorldWideWithoutPrefix" = "Global"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict index f1b55c10..0dbef076 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings index 473f3357..44f70bba 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "Takibi bırak"; "Common.Controls.Friendship.Actions.Unmute" = "Sessizden çıkar"; "Common.Controls.Friendship.BlockUser" = "%@ Engelle"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "Do you want to report and block %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "Do you want to report %@"; "Common.Controls.Friendship.Follower" = "takipçi"; "Common.Controls.Friendship.Followers" = "takipçiler"; "Common.Controls.Friendship.FollowsYou" = "Sizi takip ediyor"; @@ -254,10 +256,10 @@ "Common.Notification.Favourite" = "%@ senin gücünü beğendi"; "Common.Notification.Follow" = "%@ seni takip etti"; "Common.Notification.FollowRequest" = "%@ sana takip isteği gönderdi"; -"Common.Notification.FollowRequestAction.Approve" = "Approve"; -"Common.Notification.FollowRequestAction.Deny" = "Deny"; -"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Follow Request Approved"; -"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Follow Request Denied"; +"Common.Notification.FollowRequestAction.Approve" = "Onayla"; +"Common.Notification.FollowRequestAction.Deny" = "Reddet"; +"Common.Notification.FollowRequestResponse.FollowRequestApproved" = "Takip isteği kabul edildi"; +"Common.Notification.FollowRequestResponse.FollowRequestDenied" = "Takip isteği kabul reddedildi"; "Common.Notification.Mentions" = "%@ seni etiketledi"; "Common.Notification.Messages.Content" = "%@ sana mesaj gönderdi"; "Common.Notification.Messages.Title" = "Yeni direkt mesaj"; @@ -396,6 +398,11 @@ Hala erken aşamada."; "Scene.Settings.About.Logo.BackgroundShadow" = "Sayfa arka plan logosu gölgesi hakkında"; "Scene.Settings.About.Title" = "Hakkında"; "Scene.Settings.About.Version" = "Sür %@"; +"Scene.Settings.Account.AccountSettings" = "Account Settings"; +"Scene.Settings.Account.BlockedPeople" = "Blocked People"; +"Scene.Settings.Account.MuteAndBlock" = "Mute and Block"; +"Scene.Settings.Account.MutedPeople" = "Muted People"; +"Scene.Settings.Account.Title" = "Hesabım"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED optimize edilmiş mod"; "Scene.Settings.Appearance.AppIcon" = "Uygulama Simgesi"; "Scene.Settings.Appearance.HighlightColor" = "Rengi vurgula"; @@ -466,9 +473,16 @@ Hala erken aşamada."; "Scene.Settings.Misc.Proxy.Username" = "Kullanıcı adı"; "Scene.Settings.Misc.Title" = "Diğer"; "Scene.Settings.Notification.Accounts" = "Hesaplar"; +"Scene.Settings.Notification.Mastodon.Favorite" = "Favorite"; +"Scene.Settings.Notification.Mastodon.Mention" = "Mention"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "New Follow"; +"Scene.Settings.Notification.Mastodon.Poll" = "poll"; +"Scene.Settings.Notification.Mastodon.Reblog" = "Reblog"; "Scene.Settings.Notification.NotificationSwitch" = "Bildirim göster"; +"Scene.Settings.Notification.PushNotification" = "Push Notification"; "Scene.Settings.Notification.Title" = "Bildirim"; "Scene.Settings.SectionHeader.About" = "Hakkında"; +"Scene.Settings.SectionHeader.Account" = "Hesabım"; "Scene.Settings.SectionHeader.General" = "Genel"; "Scene.Settings.Storage.All.SubTitle" = "Tüm Twidere X önbelleğini sil. Hesap kimliğiniz kaybolmaz."; "Scene.Settings.Storage.All.Title" = "Tüm önbelleği temizle"; @@ -497,6 +511,6 @@ Başlamak için giriş yap."; "Scene.Trends.Accounts" = "%d kişi konuşuyor"; "Scene.Trends.Now" = "Trendler"; "Scene.Trends.Title" = "Trendler"; -"Scene.Trends.TrendsLocation" = "Trends Location"; +"Scene.Trends.TrendsLocation" = "Trendler Konumu"; "Scene.Trends.WorldWide" = "Trendler - Dünya Çapındaki"; "Scene.Trends.WorldWideWithoutPrefix" = "Dünya çapında"; \ No newline at end of file diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict index 971c4be6..698e117d 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict @@ -2,6 +2,22 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 notification + other + %ld notifications + + count.media NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index 4ac88fdb..e6bdafcb 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -192,6 +192,8 @@ "Common.Controls.Friendship.Actions.Unfollow" = "取消关注"; "Common.Controls.Friendship.Actions.Unmute" = "解除静音"; "Common.Controls.Friendship.BlockUser" = "屏蔽 %@"; +"Common.Controls.Friendship.DoYouWantToReportAnd BlockUser" = "是否要举报并屏蔽 %@"; +"Common.Controls.Friendship.DoYouWantToReportUser" = "是否要举报 %@"; "Common.Controls.Friendship.Follower" = "关注者"; "Common.Controls.Friendship.Followers" = "关注者"; "Common.Controls.Friendship.FollowsYou" = "关注了你"; @@ -396,6 +398,11 @@ "Scene.Settings.About.Logo.BackgroundShadow" = "关于页面背景徽标阴影"; "Scene.Settings.About.Title" = "关于"; "Scene.Settings.About.Version" = "版本 %@"; +"Scene.Settings.Account.AccountSettings" = "账户设置"; +"Scene.Settings.Account.BlockedPeople" = "已屏蔽用户"; +"Scene.Settings.Account.MuteAndBlock" = "静音和屏蔽"; +"Scene.Settings.Account.MutedPeople" = "已静音用户"; +"Scene.Settings.Account.Title" = "账号"; "Scene.Settings.Appearance.AmoledOptimizedMode" = "AMOLED 优化模式"; "Scene.Settings.Appearance.AppIcon" = "应用图标"; "Scene.Settings.Appearance.HighlightColor" = "高亮颜色"; @@ -466,9 +473,16 @@ "Scene.Settings.Misc.Proxy.Username" = "用户名"; "Scene.Settings.Misc.Title" = "其它"; "Scene.Settings.Notification.Accounts" = "帐号"; +"Scene.Settings.Notification.Mastodon.Favorite" = "喜欢"; +"Scene.Settings.Notification.Mastodon.Mention" = "提及"; +"Scene.Settings.Notification.Mastodon.NewFollow" = "新关注者"; +"Scene.Settings.Notification.Mastodon.Poll" = "投票"; +"Scene.Settings.Notification.Mastodon.Reblog" = "转发"; "Scene.Settings.Notification.NotificationSwitch" = "显示通知"; +"Scene.Settings.Notification.PushNotification" = "推送通知"; "Scene.Settings.Notification.Title" = "通知"; "Scene.Settings.SectionHeader.About" = "关于"; +"Scene.Settings.SectionHeader.Account" = "账号"; "Scene.Settings.SectionHeader.General" = "通用"; "Scene.Settings.Storage.All.SubTitle" = "删除所有 Twidere X 缓存。您的帐户凭据不会丢失。"; "Scene.Settings.Storage.All.Title" = "清除所有缓存"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict index 3bf18685..4b4c4e1b 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict @@ -2,6 +2,20 @@ + count.notification + + NSStringLocalizedFormatKey + %#@count.notification@ + count.notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个通知 + + count.media NSStringLocalizedFormatKey diff --git a/TwidereX/Resources/ar.lproj/InfoPlist.strings b/TwidereX/Resources/ar.lproj/InfoPlist.strings index 7ff80e01..420afa7e 100644 --- a/TwidereX/Resources/ar.lproj/InfoPlist.strings +++ b/TwidereX/Resources/ar.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "أكتب"; "NSCameraUsageDescription" = "يستخدم لالتقاط صورة للتغريدة"; "NSLocationWhenInUseUsageDescription" = "يستخدم لإرسال تغريدة محددة جغرافيًا"; -"NSPhotoLibraryAddUsageDescription" = "يستخدم لحفظ الصورة في مكتبة الصور"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "يستخدم لحفظ الصورة في مكتبة الصور"; +"SearchShortcutItemTitle" = "البحث"; \ No newline at end of file diff --git a/TwidereX/Resources/ca.lproj/InfoPlist.strings b/TwidereX/Resources/ca.lproj/InfoPlist.strings index a577bfe7..235ffd54 100644 --- a/TwidereX/Resources/ca.lproj/InfoPlist.strings +++ b/TwidereX/Resources/ca.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Redacta"; "NSCameraUsageDescription" = "Emprat per fer una foto per a la piulada"; "NSLocationWhenInUseUsageDescription" = "Emprat per enviar una piulada amb ubicació"; -"NSPhotoLibraryAddUsageDescription" = "Emprat per desar una foto a la Galeria d'imatges"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Emprat per desar una foto a la Galeria d'imatges"; +"SearchShortcutItemTitle" = "Cerca"; \ No newline at end of file diff --git a/TwidereX/Resources/de.lproj/InfoPlist.strings b/TwidereX/Resources/de.lproj/InfoPlist.strings index 5899d5ce..9ee8720e 100644 --- a/TwidereX/Resources/de.lproj/InfoPlist.strings +++ b/TwidereX/Resources/de.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Erstellen"; "NSCameraUsageDescription" = "Für Tweet Foto machen"; "NSLocationWhenInUseUsageDescription" = "Wird zum Senden von geomarkierten Tweets verwendet"; -"NSPhotoLibraryAddUsageDescription" = "Wird zum Speichern von Fotos in der Fotogalerie verwendet"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Wird zum Speichern von Fotos in der Fotogalerie verwendet"; +"SearchShortcutItemTitle" = "Suche"; \ No newline at end of file diff --git a/TwidereX/Resources/en.lproj/InfoPlist.strings b/TwidereX/Resources/en.lproj/InfoPlist.strings index c2a83e86..79742067 100644 --- a/TwidereX/Resources/en.lproj/InfoPlist.strings +++ b/TwidereX/Resources/en.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Compose"; "NSCameraUsageDescription" = "Used to take photo for tweet"; "NSLocationWhenInUseUsageDescription" = "Used to send geo marked tweet"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; +"SearchShortcutItemTitle" = "Search"; \ No newline at end of file diff --git a/TwidereX/Resources/es.lproj/InfoPlist.strings b/TwidereX/Resources/es.lproj/InfoPlist.strings index e163a321..26dccc5a 100644 --- a/TwidereX/Resources/es.lproj/InfoPlist.strings +++ b/TwidereX/Resources/es.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Crear"; "NSCameraUsageDescription" = "Realizar una foto para el tweet"; "NSLocationWhenInUseUsageDescription" = "Enviar geolocalización en el tweet"; -"NSPhotoLibraryAddUsageDescription" = "Guardar foto en la galería"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Guardar foto en la galería"; +"SearchShortcutItemTitle" = "Buscar"; \ No newline at end of file diff --git a/TwidereX/Resources/eu.lproj/InfoPlist.strings b/TwidereX/Resources/eu.lproj/InfoPlist.strings index eb802e59..5b81645b 100644 --- a/TwidereX/Resources/eu.lproj/InfoPlist.strings +++ b/TwidereX/Resources/eu.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Idatzi"; "NSCameraUsageDescription" = "Tweeterako argazkia ateratzeko erabiltzen da"; "NSLocationWhenInUseUsageDescription" = "Geografikoki markatutako tweet bat bidaltzeko erabiltzen da"; -"NSPhotoLibraryAddUsageDescription" = "Argazki-liburutegian gordetzeko balio du"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Argazki-liburutegian gordetzeko balio du"; +"SearchShortcutItemTitle" = "Bilatu"; \ No newline at end of file diff --git a/TwidereX/Resources/gl.lproj/InfoPlist.strings b/TwidereX/Resources/gl.lproj/InfoPlist.strings index 842d7db3..bcb0b7da 100644 --- a/TwidereX/Resources/gl.lproj/InfoPlist.strings +++ b/TwidereX/Resources/gl.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Redactar"; "NSCameraUsageDescription" = "Usado para tirar foto para chiar"; "NSLocationWhenInUseUsageDescription" = "Usado para enviar chío con localización"; -"NSPhotoLibraryAddUsageDescription" = "Usado para gardar foto na Galería"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Usado para gardar foto na Galería"; +"SearchShortcutItemTitle" = "Buscar"; \ No newline at end of file diff --git a/TwidereX/Resources/ja.lproj/InfoPlist.strings b/TwidereX/Resources/ja.lproj/InfoPlist.strings index 89d7333c..207a12ca 100644 --- a/TwidereX/Resources/ja.lproj/InfoPlist.strings +++ b/TwidereX/Resources/ja.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "新規作成"; "NSCameraUsageDescription" = "ツイート用の写真を撮るために使用"; "NSLocationWhenInUseUsageDescription" = "位置情報付きのツイートのために使用"; -"NSPhotoLibraryAddUsageDescription" = "フォトライブラリに写真を保存するために使用"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "フォトライブラリに写真を保存するために使用"; +"SearchShortcutItemTitle" = "検索"; \ No newline at end of file diff --git a/TwidereX/Resources/ko.lproj/InfoPlist.strings b/TwidereX/Resources/ko.lproj/InfoPlist.strings index 36e8ddc7..3de2d513 100644 --- a/TwidereX/Resources/ko.lproj/InfoPlist.strings +++ b/TwidereX/Resources/ko.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "쓰기"; "NSCameraUsageDescription" = "트윗에 올릴 사진을 찍기 위해 쓰임"; "NSLocationWhenInUseUsageDescription" = "위치 기록 트윗에 쓰임"; -"NSPhotoLibraryAddUsageDescription" = "갤러리에 사진을 저장하기 위해 쓰임"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "갤러리에 사진을 저장하기 위해 쓰임"; +"SearchShortcutItemTitle" = "찾기"; \ No newline at end of file diff --git a/TwidereX/Resources/pt-BR.lproj/InfoPlist.strings b/TwidereX/Resources/pt-BR.lproj/InfoPlist.strings index 45bb47b4..b8139e89 100644 --- a/TwidereX/Resources/pt-BR.lproj/InfoPlist.strings +++ b/TwidereX/Resources/pt-BR.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Compor"; "NSCameraUsageDescription" = "Usado para tirar foto para tweet"; "NSLocationWhenInUseUsageDescription" = "Usado para enviar tweet com localização marcada"; -"NSPhotoLibraryAddUsageDescription" = "Usado para salvar fotos na Biblioteca de Fotos"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Usado para salvar fotos na Biblioteca de Fotos"; +"SearchShortcutItemTitle" = "Buscar"; \ No newline at end of file diff --git a/TwidereX/Resources/tr.lproj/InfoPlist.strings b/TwidereX/Resources/tr.lproj/InfoPlist.strings index 6fd19f64..01f143be 100644 --- a/TwidereX/Resources/tr.lproj/InfoPlist.strings +++ b/TwidereX/Resources/tr.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "Oluştur"; "NSCameraUsageDescription" = "Tweet için fotoğraf çekmekte kullanılır"; "NSLocationWhenInUseUsageDescription" = "Coğrafi işaretli tweet göndermekte kullanılır"; -"NSPhotoLibraryAddUsageDescription" = "Fotoğrafı Fotoğraf Kitaplığına kaydetmekte kullanılır"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "Fotoğrafı Fotoğraf Kitaplığına kaydetmekte kullanılır"; +"SearchShortcutItemTitle" = "Ara"; \ No newline at end of file diff --git a/TwidereX/Resources/zh-Hans.lproj/InfoPlist.strings b/TwidereX/Resources/zh-Hans.lproj/InfoPlist.strings index 71d7abff..2d6643d5 100644 --- a/TwidereX/Resources/zh-Hans.lproj/InfoPlist.strings +++ b/TwidereX/Resources/zh-Hans.lproj/InfoPlist.strings @@ -1,3 +1,5 @@ +"ComposeShortcutItemTitle" = "撰写"; "NSCameraUsageDescription" = "用于发送拍照推文"; "NSLocationWhenInUseUsageDescription" = "用于发送含地理标记的推文"; -"NSPhotoLibraryAddUsageDescription" = "用于将照片保存到照片库"; \ No newline at end of file +"NSPhotoLibraryAddUsageDescription" = "用于将照片保存到照片库"; +"SearchShortcutItemTitle" = "搜索"; \ No newline at end of file diff --git a/TwidereXIntent/gl.lproj/Intents.strings b/TwidereXIntent/gl.lproj/Intents.strings index 4b08a210..31ccb3e3 100644 --- a/TwidereXIntent/gl.lproj/Intents.strings +++ b/TwidereXIntent/gl.lproj/Intents.strings @@ -56,7 +56,7 @@ "aeaw8w" = "Directo"; -"ch7ADX" = "Compose new post"; +"ch7ADX" = "Escribir nova publicación"; "f1YNIs" = "Publicar contido"; diff --git a/TwidereXIntent/ko.lproj/Intents.strings b/TwidereXIntent/ko.lproj/Intents.strings index fc197373..81b9ac5c 100644 --- a/TwidereXIntent/ko.lproj/Intents.strings +++ b/TwidereXIntent/ko.lproj/Intents.strings @@ -12,61 +12,61 @@ "8WWS78" = "사용자 이름"; -"9OV1Ic" = "Posts"; +"9OV1Ic" = "게시글"; -"ApmQKm" = "Switch Account"; +"ApmQKm" = "계정 전환"; "BS59z4" = "공개"; "Ciuk7c" = "이름"; -"DBgjXT" = "Account"; +"DBgjXT" = "계정"; -"Fn8AQn" = "URL"; +"Fn8AQn" = "링크"; -"G5v3xr" = "Set active account"; +"G5v3xr" = "계정 활성화"; -"GILN5g" = "There are ${count} options matching ‘${accounts}’."; +"GILN5g" = "${count}개의 설정이 '${accounts}'(와)과 일치합니다."; "N4GVJI" = "${errorDescription}"; -"QSA5ql" = "Account"; +"QSA5ql" = "계정"; -"QWcTQP" = "Switch to ${account}"; +"QWcTQP" = "${account}로 전환"; -"SnCJhk" = "Publish Post"; +"SnCJhk" = "공개 게시"; "SqRrlA" = "Just to confirm, you wanted ‘${accounts}’?"; -"SrV2FP" = "Error Description"; +"SrV2FP" = "오류 설명"; -"TzDamL" = "Toot Visibility"; +"TzDamL" = "툿 공개범위"; "WVP0I1" = "${errorDescription}"; -"Xs6a35" = "Toot Visibility"; +"Xs6a35" = "툿 공개범위"; "YNUo2I" = "There are ${count} options matching ‘${account}’."; "Yb4tzx" = "계정"; -"aV5Ezl" = "Default"; +"aV5Ezl" = "기본값"; "aV8f0g" = "비공개"; "aeaw8w" = "직접"; -"ch7ADX" = "Compose new post"; +"ch7ADX" = "새로 글쓰기"; -"f1YNIs" = "Post Content"; +"f1YNIs" = "작성할 콘텐츠"; "jEAHra" = "리스트 되지 않음"; -"kWl9zU" = "Post ${content}with ${accounts}"; +"kWl9zU" = "${accounts}(와)과 함께 ${content} 작성"; -"noeHVX" = "Post"; +"noeHVX" = "게시글"; -"vbgnbQ" = "Post ${content}with ${accounts}"; +"vbgnbQ" = "${accounts}(와)과 함께 ${content} 작성"; "xeqcBa-BS59z4" = "There are ${count} options matching ‘Public’."; @@ -78,6 +78,6 @@ "xeqcBa-jEAHra" = "There are ${count} options matching ‘Unlisted’."; -"z3DAP7" = "Switch to ${account}"; +"z3DAP7" = "${account}로 전환"; -"zlMGvn" = "Content"; +"zlMGvn" = "콘텐츠"; From f9026d63ab91439f18bc3ef5b72122d7ab92761c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 15:30:31 +0800 Subject: [PATCH 29/38] chore: use i18n words --- .../AccountPreferenceView.swift | 8 ++++---- .../MastodonNotificationSectionView.swift | 16 ++++++++-------- TwidereX/Supporting Files/SceneDelegate.swift | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift index f735903a..f185906c 100644 --- a/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/AccountPreferenceView.swift @@ -18,9 +18,9 @@ enum AccountPreferenceListEntry: Hashable { var title: String { switch self { - case .muted: return "Muted People" - case .blocked: return "Blocked People" - case .accountSettings: return "Account Settings" + case .muted: return L10n.Scene.Settings.Account.mutedPeople + case .blocked: return L10n.Scene.Settings.Account.blockedPeople + case .accountSettings: return L10n.Scene.Settings.Account.accountSettings case .signout: return L10n.Common.Controls.Actions.signOut } } @@ -33,7 +33,7 @@ struct AccountPreferenceView: View { @State var isPushNotificationEnabled = true var muteAndBlockSection: some View { - Section(header: Text("Mute and Block")) { + Section(header: Text(L10n.Scene.Settings.Account.muteAndBlock)) { let entries: [AccountPreferenceListEntry] = [ .muted, .blocked ] diff --git a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift index f10e7434..ec17566a 100644 --- a/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift +++ b/TwidereX/Scene/Setting/AccountPreference/MastodonNotification/MastodonNotificationSectionView.swift @@ -19,7 +19,7 @@ struct MastodonNotificationSectionView: View { Group { // push notification secion Section { - Toggle("Push Notification", isOn: Binding( + Toggle(L10n.Scene.Settings.Notification.pushNotification, isOn: Binding( get: { viewModel.isActive }, set: { newValue in viewModel.isActive = newValue @@ -47,8 +47,8 @@ struct MastodonNotificationSectionView: View { extension MastodonNotificationSectionView { var notificationOptionSection: some View { - Section(header: Text("Notifications")) { - Toggle("New Follow", isOn: Binding( + Section(header: Text(L10n.Scene.Settings.Notification.title)) { + Toggle(L10n.Scene.Settings.Notification.Mastodon.newFollow, isOn: Binding( get: { viewModel.isNewFollowEnabled }, set: { newValue in viewModel.isNewFollowEnabled = newValue @@ -57,7 +57,7 @@ extension MastodonNotificationSectionView { } } )) - Toggle("Reblog", isOn: Binding( + Toggle(L10n.Scene.Settings.Notification.Mastodon.reblog, isOn: Binding( get: { viewModel.isReblogEnabled }, set: { newValue in viewModel.isReblogEnabled = newValue @@ -66,7 +66,7 @@ extension MastodonNotificationSectionView { } } )) - Toggle("Favorite", isOn: Binding( + Toggle(L10n.Scene.Settings.Notification.Mastodon.favorite, isOn: Binding( get: { viewModel.isFavoriteEnabled }, set: { newValue in viewModel.isFavoriteEnabled = newValue @@ -75,7 +75,7 @@ extension MastodonNotificationSectionView { } } )) - Toggle("Poll", isOn: Binding( + Toggle(L10n.Scene.Settings.Notification.Mastodon.poll, isOn: Binding( get: { viewModel.isPollEnabled }, set: { newValue in viewModel.isPollEnabled = newValue @@ -84,7 +84,7 @@ extension MastodonNotificationSectionView { } } )) - Toggle("Mention", isOn: Binding( + Toggle(L10n.Scene.Settings.Notification.Mastodon.mention, isOn: Binding( get: { viewModel.isMentionEnabled }, set: { newValue in viewModel.isMentionEnabled = newValue @@ -98,7 +98,7 @@ extension MastodonNotificationSectionView { var mentionPreferenceSection: some View { Section( - header: Text("Notifications: Mention"), + header: Text("Notifications: Mention"), // TODO: i18n footer: Group { switch viewModel.mentionPreference { diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index cb1778f0..86e087d2 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -152,7 +152,7 @@ extension SceneDelegate { switch shortcutItem.type { - case "com.twidere.TwidereX.new-post": + case "com.twidere.TwidereX.compose": if let topMost = topMostViewController(), topMost.isModal { topMost.dismiss(animated: false) } From 145ba09f60c0d9d25b61b715da588b8805a3832c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 15:53:31 +0800 Subject: [PATCH 30/38] chore: update version to 1.4.2 (113) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 98891f08..ef5abbe2 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 112 + 113 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index e8ad8398..294b13f0 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 112 + 113 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 8f01d6a4..2b0a027a 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3550,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3576,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3599,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3625,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 112; + DYLIB_CURRENT_VERSION = 113; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3657,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3684,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3714,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3744,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3773,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3802,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 112; + DYLIB_CURRENT_VERSION = 113; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3836,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 112; + DYLIB_CURRENT_VERSION = 113; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3867,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3894,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4048,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4079,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4105,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4130,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4153,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4176,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4200,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4228,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 112; + CURRENT_PROJECT_VERSION = 113; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 41265019..e122353f 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 112 + 113 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index b49737e8..44dab60e 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 112 + 113 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 4c357737..2e513d35 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 112 + 113 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 4c357737..2e513d35 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 112 + 113 From 9b34660308859e728ce8598be869a15aae58703d Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 18:02:48 +0800 Subject: [PATCH 31/38] fix: shortcut subtitle i18n issue --- .../Sources/TwidereCore/Service/NotificationService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index 9f09c87e..fb7e7db9 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -114,7 +114,7 @@ extension NotificationService { public nonisolated func unreadApplicationShortcutItems() async -> [UIApplicationShortcutItem] { guard let authenticationService = await self.authenticationService else { return [] } let managedObjectContext = authenticationService.managedObjectContext - return await managedObjectContext.perform{ + return await managedObjectContext.perform { var items: [UIApplicationShortcutItem] = [] for object in authenticationService.authenticationIndexes { guard let authenticationIndex = managedObjectContext.object(with: object.objectID) as? AuthenticationIndex else { continue } @@ -128,7 +128,7 @@ extension NotificationService { guard let user = authenticationIndex.user else { continue} let title = "@\(user.username)" - let subtitle = "\(count) notifications" + let subtitle = L10n.Count.notification(count) let item = UIApplicationShortcutItem( type: NotificationService.unreadShortcutItemIdentifier, From 13596f2d6da300715fbf41b78e5afe662379cb5b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 22 Jul 2022 18:38:35 +0800 Subject: [PATCH 32/38] chore: update version to 1.4.2 (114) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- .../Service/NotificationService.swift | 1 + TwidereX.xcodeproj/project.pbxproj | 48 +++++++++---------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 8 files changed, 31 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index ef5abbe2..d8c49e8a 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 113 + 114 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 294b13f0..99c638c3 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 113 + 114 NSExtension NSExtensionAttributes diff --git a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift index fb7e7db9..08703fe5 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/NotificationService.swift @@ -11,6 +11,7 @@ import Combine import CoreData import CoreDataStack import TwidereCommon +import TwidereLocalization final public actor NotificationService { diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 2b0a027a..7625a9e8 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3550,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3576,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3599,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3625,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 113; + DYLIB_CURRENT_VERSION = 114; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3657,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3684,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3714,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3744,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3773,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3802,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 113; + DYLIB_CURRENT_VERSION = 114; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3836,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 113; + DYLIB_CURRENT_VERSION = 114; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3867,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3894,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4048,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4079,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4105,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4130,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4153,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4176,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4200,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4228,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 113; + CURRENT_PROJECT_VERSION = 114; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index e122353f..c407d1fe 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 113 + 114 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 44dab60e..80ade7b1 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 113 + 114 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 2e513d35..c168a047 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 113 + 114 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 2e513d35..c168a047 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 113 + 114 From 9dc7104b0000ae515ee072531c134fa4de610114 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 23 Jul 2022 00:49:03 +0800 Subject: [PATCH 33/38] fix: i18n stringsdict key format issue --- .../Sources/TwidereLocalization/Generated/Strings.swift | 2 +- .../Resources/ar.lproj/Localizable.stringsdict | 4 ++-- .../Resources/ca.lproj/Localizable.stringsdict | 4 ++-- .../Resources/de.lproj/Localizable.stringsdict | 4 ++-- .../Resources/en.lproj/Localizable.stringsdict | 4 ++-- .../Resources/es.lproj/Localizable.stringsdict | 4 ++-- .../Resources/eu.lproj/Localizable.stringsdict | 4 ++-- .../Resources/gl.lproj/Localizable.stringsdict | 4 ++-- .../Resources/ja.lproj/Localizable.stringsdict | 4 ++-- .../Resources/ko.lproj/Localizable.stringsdict | 4 ++-- .../Resources/pt-BR.lproj/Localizable.stringsdict | 4 ++-- .../Resources/tr.lproj/Localizable.stringsdict | 4 ++-- .../Resources/zh-Hans.lproj/Localizable.stringsdict | 4 ++-- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index 106a3766..9b34eb7a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -1631,7 +1631,7 @@ public enum L10n { public static func media(_ p1: Int) -> String { return L10n.tr("Localizable", "count.media", p1) } - /// Plural format key: "%#@count.notification@" + /// Plural format key: "%#@count_notification@" public static func notification(_ p1: Int) -> String { return L10n.tr("Localizable", "count.notification", p1) } diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict index 2d7c8180..7967374e 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict index 7c65d2a5..79dc0d21 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict index b55a4b7c..2ef23d97 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict index 7c65d2a5..79dc0d21 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict index 75bdbf87..b4cf04f6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict index 09c127db..3c79e9de 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/eu.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict index f73ac52c..230796c1 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/gl.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict index 16cfe3c8..26c99e7a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict index 0e94be8e..207ee503 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict index 0dbef076..149368ad 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict index 698e117d..d90abaa6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/tr.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict index 4b4c4e1b..c271b9b6 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict @@ -5,8 +5,8 @@ count.notification NSStringLocalizedFormatKey - %#@count.notification@ - count.notification + %#@count_notification@ + count_notification NSStringFormatSpecTypeKey NSStringPluralRuleType From d104980365494845ee047a329f4bca924d044e93 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 23 Jul 2022 00:49:46 +0800 Subject: [PATCH 34/38] chore: update version to 1.4.2 (115) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index d8c49e8a..405468d0 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 114 + 115 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 99c638c3..3f56fc6f 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 114 + 115 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 7625a9e8..8e2686bf 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3550,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3576,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3599,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3625,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 115; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3657,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3684,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3714,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3744,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3773,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3802,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 115; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3836,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 114; + DYLIB_CURRENT_VERSION = 115; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3867,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3894,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4048,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4079,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4105,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4130,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4153,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4176,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4200,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4228,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 114; + CURRENT_PROJECT_VERSION = 115; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index c407d1fe..05a15b31 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 114 + 115 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 80ade7b1..5a05712c 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 114 + 115 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index c168a047..733b526f 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 114 + 115 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index c168a047..733b526f 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 114 + 115 From c7b5c36863d83676b8d0621c75c75d61456aac8a Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 23 Jul 2022 16:44:10 +0800 Subject: [PATCH 35/38] fix: shortcut not trigger account switch issue --- TwidereX/Supporting Files/SceneDelegate.swift | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 86e087d2..31c2e910 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -186,9 +186,23 @@ extension SceneDelegate { coordinator.switchToTabBar(tab: .search) return true case NotificationService.unreadShortcutItemIdentifier: - if let topMost = topMostViewController(), topMost.isModal { - topMost.dismiss(animated: false) + guard let accessToken = shortcutItem.userInfo?["accessToken"] as? String else { + assertionFailure() + return false + } + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) + request.fetchLimit = 1 + guard let authentication = try? coordinator.context.managedObjectContext.fetch(request).first else { + assertionFailure() + return false + } + + let _isActive = try? await coordinator.context.authenticationService.activeAuthenticationIndex(record: authentication.authenticationIndex.asRecrod) + guard _isActive == true else { + return false } + coordinator.switchToTabBar(tab: .notification) return true default: From 13433de9e60ef3e348f7fc7c347eaf035aca6da2 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 23 Jul 2022 16:45:05 +0800 Subject: [PATCH 36/38] chore: update to version 1.4.2 (116) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 405468d0..82b2d634 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 115 + 116 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 3f56fc6f..ebfed7e8 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 115 + 116 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 8e2686bf..28dd8954 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3550,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3576,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3599,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3625,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 115; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3657,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3684,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3714,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3744,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3773,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3802,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 115; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3836,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 115; + DYLIB_CURRENT_VERSION = 116; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3867,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3894,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4048,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4079,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4105,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4130,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4153,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4176,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4200,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4228,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 115; + CURRENT_PROJECT_VERSION = 116; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 05a15b31..1904b08a 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 115 + 116 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 5a05712c..852e5972 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 115 + 116 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 733b526f..c5ba6cd2 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 115 + 116 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 733b526f..c5ba6cd2 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 115 + 116 From 427d7e481babddcbe28c0a70d772c1d802aa805b Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 25 Jul 2022 10:56:57 +0800 Subject: [PATCH 37/38] chore: code cleanup --- TwidereX/Supporting Files/SceneDelegate.swift | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/TwidereX/Supporting Files/SceneDelegate.swift b/TwidereX/Supporting Files/SceneDelegate.swift index 31c2e910..410817dd 100644 --- a/TwidereX/Supporting Files/SceneDelegate.swift +++ b/TwidereX/Supporting Files/SceneDelegate.swift @@ -211,41 +211,6 @@ extension SceneDelegate { } } -// private func handler(shortcutItem: UIApplicationShortcutItem) async -> Bool { -// -// switch shortcutItem.type { -// case "org.joinmastodon.app.new-post": -// if coordinator?.tabBarController.topMost is ComposeViewController { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…") -// } else { -// if let authenticationBox = AppContext.shared.authenticationService.activeMastodonAuthenticationBox.value { -// let composeViewModel = ComposeViewModel( -// context: AppContext.shared, -// composeKind: .post, -// authenticationBox: authenticationBox -// ) -// coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") -// } else { -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") -// } -// } -// case "org.joinmastodon.app.search": -// coordinator?.switchToTabBar(tab: .search) -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select search tab") -// -// if let searchViewController = coordinator?.tabBarController.topMost as? SearchViewController { -// searchViewController.searchBarTapPublisher.send() -// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search") -// } -// default: -// assertionFailure() -// break -// } -// -// return true -// } - } #if DEBUG From 1f39bf557c0571e95fe3c007124433094fdc3d06 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 25 Jul 2022 10:57:14 +0800 Subject: [PATCH 38/38] chore: update version to 1.4.2 (117) --- NotificationService/Info.plist | 2 +- ShareExtension/Info.plist | 2 +- TwidereX.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- TwidereX/Info.plist | 2 +- TwidereXIntent/Info.plist | 2 +- TwidereXTests/Info.plist | 2 +- TwidereXUITests/Info.plist | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 82b2d634..8fea3554 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleVersion - 116 + 117 NSExtension NSExtensionPointIdentifier diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index ebfed7e8..c02f1cde 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 116 + 117 NSExtension NSExtensionAttributes diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 28dd8954..cdfe207a 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -3550,7 +3550,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3576,7 +3576,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3599,7 +3599,7 @@ baseConfigurationReference = BC377CE93F0DE1E07208F697 /* Pods-TwidereX-TwidereXUITests.profile.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3625,11 +3625,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 116; + DYLIB_CURRENT_VERSION = 117; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3657,7 +3657,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3684,7 +3684,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -3714,7 +3714,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; @@ -3744,7 +3744,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3773,7 +3773,7 @@ CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NotificationService/Info.plist; @@ -3802,11 +3802,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 116; + DYLIB_CURRENT_VERSION = 117; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3836,11 +3836,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 116; + DYLIB_CURRENT_VERSION = 117; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3867,7 +3867,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3894,7 +3894,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -4048,7 +4048,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4079,7 +4079,7 @@ CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -4105,7 +4105,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4130,7 +4130,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = Twidere; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4153,7 +4153,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4176,7 +4176,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -4200,7 +4200,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; @@ -4228,7 +4228,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = TwidereXIntent/TwidereXIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 116; + CURRENT_PROJECT_VERSION = 117; DEVELOPMENT_TEAM = 7LFDZ96332; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TwidereXIntent/Info.plist; diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 1904b08a..e8195054 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -40,7 +40,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 116 + 117 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereXIntent/Info.plist b/TwidereXIntent/Info.plist index 852e5972..74bdbc2e 100644 --- a/TwidereXIntent/Info.plist +++ b/TwidereXIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 116 + 117 NSExtension NSExtensionAttributes diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index c5ba6cd2..2ee4462a 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 116 + 117 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index c5ba6cd2..2ee4462a 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.2 CFBundleVersion - 116 + 117