diff --git a/ShareExtension/Info.plist b/ShareExtension/Info.plist index 7b8ce5bd..3390cf12 100644 --- a/ShareExtension/Info.plist +++ b/ShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.4 + 1.2.6 CFBundleVersion - 74 + 77 NSExtension NSExtensionAttributes diff --git a/TwidereSDK/Package.swift b/TwidereSDK/Package.swift index ce2c827f..0699f50a 100644 --- a/TwidereSDK/Package.swift +++ b/TwidereSDK/Package.swift @@ -31,11 +31,11 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.34.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/MainasuK/CommonOSLog", from: "0.1.1"), - .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("3.2.1")), - .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"), + .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("3.3.1")), + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.5.0"), .package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"), .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.1.1"), - .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.1"), + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"), .package(url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"), .package(url: "https://github.com/TimOliver/TOCropViewController.git", from: "2.6.0"), .package(url: "https://github.com/MainasuK/KeyboardLayoutGuide.git", branch: "fix/iOS15"), diff --git a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 4.xcdatamodel/contents b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 4.xcdatamodel/contents index f282c668..deb57f40 100644 --- a/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 4.xcdatamodel/contents +++ b/TwidereSDK/Sources/CoreDataStack/CoreDataStack.xcdatamodeld/CoreDataStack 4.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -85,7 +85,7 @@ - + @@ -179,6 +179,7 @@ + @@ -243,7 +244,7 @@ - + \ No newline at end of file diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift index f2fcfce6..7d723748 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Mastodon/MastodonStatus.swift @@ -45,7 +45,7 @@ final public class MastodonStatus: NSManagedObject { @NSManaged public private(set) var isMediaSensitive: Bool // sourcery: autoUpdatableObject - @NSManaged public private(set) var isContentReveal: Bool + @NSManaged public private(set) var isContentSensitiveToggled: Bool // sourcery: autoUpdatableObject @NSManaged public private(set) var isMediaSensitiveToggled: Bool @@ -382,9 +382,9 @@ extension MastodonStatus: AutoUpdatableObject { self.isMediaSensitive = isMediaSensitive } } - public func update(isContentReveal: Bool) { - if self.isContentReveal != isContentReveal { - self.isContentReveal = isContentReveal + public func update(isContentSensitiveToggled: Bool) { + if self.isContentSensitiveToggled != isContentSensitiveToggled { + self.isContentSensitiveToggled = isContentSensitiveToggled } } public func update(isMediaSensitiveToggled: Bool) { diff --git a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift index c33f774d..4466c4a1 100644 --- a/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift +++ b/TwidereSDK/Sources/CoreDataStack/Entity/Twitter/TwitterStatus.swift @@ -24,6 +24,11 @@ final public class TwitterStatus: NSManagedObject { @NSManaged public private(set) var replyCount: Int64 // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var repostCount: Int64 + // sourcery: autoGenerateProperty + @NSManaged public private(set) var quoteCount: Int64 + + // Note: not mark `autoUpdatableObject` for `replyCount` and `quoteCount` + // to avoid V1 API update the exists value to 0 // sourcery: autoUpdatableObject, autoGenerateProperty @NSManaged public private(set) var source: String? @@ -191,6 +196,7 @@ extension TwitterStatus: AutoGenerateProperty { public let likeCount: Int64 public let replyCount: Int64 public let repostCount: Int64 + public let quoteCount: Int64 public let source: String? public let replyToStatusID: TwitterStatus.ID? public let replyToUserID: TwitterUser.ID? @@ -203,6 +209,7 @@ extension TwitterStatus: AutoGenerateProperty { likeCount: Int64, replyCount: Int64, repostCount: Int64, + quoteCount: Int64, source: String?, replyToStatusID: TwitterStatus.ID?, replyToUserID: TwitterUser.ID?, @@ -214,6 +221,7 @@ extension TwitterStatus: AutoGenerateProperty { self.likeCount = likeCount self.replyCount = replyCount self.repostCount = repostCount + self.quoteCount = quoteCount self.source = source self.replyToStatusID = replyToStatusID self.replyToUserID = replyToUserID @@ -228,6 +236,7 @@ extension TwitterStatus: AutoGenerateProperty { self.likeCount = property.likeCount self.replyCount = property.replyCount self.repostCount = property.repostCount + self.quoteCount = property.quoteCount self.source = property.source self.replyToStatusID = property.replyToStatusID self.replyToUserID = property.replyToUserID @@ -351,6 +360,18 @@ extension TwitterStatus: AutoUpdatableObject { } // sourcery:end + public func update(replyCount: Int64) { + if self.replyCount != replyCount { + self.replyCount = replyCount + } + } + + public func update(quoteCount: Int64) { + if self.quoteCount != quoteCount { + self.quoteCount = quoteCount + } + } + public func update(isRepost: Bool, by user: TwitterUser) { if isRepost { if !repostBy.contains(user) { diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/Contents.json new file mode 100644 index 00000000..f5d69410 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "circle.mask.44.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/circle.mask.44.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/circle.mask.44.pdf new file mode 100644 index 00000000..991cbdde Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.44.imageset/circle.mask.44.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.88.imageset/Contents.json similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.imageset/Contents.json rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.88.imageset/Contents.json diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.imageset/circle.mask.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.88.imageset/circle.mask.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.imageset/circle.mask.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mask.88.imageset/circle.mask.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/Contents.json index 34f095f7..921b2a29 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "badge.mastodon.pdf", + "filename" : "circle.mastodon.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/badge.mastodon.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/badge.mastodon.pdf deleted file mode 100644 index efd13f26..00000000 Binary files a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/badge.mastodon.pdf and /dev/null differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/circle.mastodon.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/circle.mastodon.pdf new file mode 100644 index 00000000..5e999e61 Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.mastodon.imageset/circle.mastodon.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/Contents.json index 894120fd..04f0cfc2 100644 --- a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/Contents.json +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "badge.twitter.pdf", + "filename" : "circle.twitter.pdf", "idiom" : "universal" } ], diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/badge.twitter.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/badge.twitter.pdf deleted file mode 100644 index ec0cda3f..00000000 Binary files a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/badge.twitter.pdf and /dev/null differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/circle.twitter.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/circle.twitter.pdf new file mode 100644 index 00000000..b1afc4be Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/circle.twitter.imageset/circle.twitter.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.imageset/robot.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.imageset/robot.pdf index 80604d57..8c4f2e45 100644 Binary files a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.imageset/robot.pdf and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.imageset/robot.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/Contents.json new file mode 100644 index 00000000..5890f57b --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "robot.mask.44.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/robot.mask.44.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/robot.mask.44.pdf new file mode 100644 index 00000000..9660ddd0 Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.44.imageset/robot.mask.44.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.88.imageset/Contents.json similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.imageset/Contents.json rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.88.imageset/Contents.json diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.imageset/robot.mask.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.88.imageset/robot.mask.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.imageset/robot.mask.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/robot.mask.88.imageset/robot.mask.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.imageset/verified.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.imageset/verified.pdf index a644e0b3..5fc1201d 100644 Binary files a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.imageset/verified.pdf and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.imageset/verified.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/Contents.json new file mode 100644 index 00000000..1dcd4299 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "verified.mask.44.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/verified.mask.44.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/verified.mask.44.pdf new file mode 100644 index 00000000..67c26dd4 Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.44.imageset/verified.mask.44.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.88.imageset/Contents.json similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.imageset/Contents.json rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.88.imageset/Contents.json diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.imageset/verified.mask.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.88.imageset/verified.mask.pdf similarity index 100% rename from TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.imageset/verified.mask.pdf rename to TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Badge/verified.mask.88.imageset/verified.mask.pdf diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/Contents.json new file mode 100644 index 00000000..73e91fda --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "person.large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/person.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/person.large.pdf new file mode 100644 index 00000000..86949eae Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Human/person.large.imageset/person.large.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/Contents.json new file mode 100644 index 00000000..771d600b --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bell.large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/bell.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/bell.large.pdf new file mode 100644 index 00000000..55e909ab Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.large.imageset/bell.large.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.large.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.large.imageset/Contents.json new file mode 100644 index 00000000..577fceb6 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "bell.ringing.large.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.large.imageset/bell.ringing.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.large.imageset/bell.ringing.large.pdf new file mode 100644 index 00000000..9604d69a Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/bell.ringing.large.imageset/bell.ringing.large.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/Contents.json new file mode 100644 index 00000000..d5ca57a0 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "home.large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/home.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/home.large.pdf new file mode 100644 index 00000000..53dbbe2d Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/house.large.imageset/home.large.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/Contents.json b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/Contents.json new file mode 100644 index 00000000..213ca8d4 --- /dev/null +++ b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "magnifyingglass.large.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/magnifyingglass.large.pdf b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/magnifyingglass.large.pdf new file mode 100644 index 00000000..76f6c5d0 Binary files /dev/null and b/TwidereSDK/Sources/TwidereAsset/Assets.xcassets/Object&Tools/magnifyingglass.large.imageset/magnifyingglass.large.pdf differ diff --git a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift index dc3ba27e..ff091473 100644 --- a/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift +++ b/TwidereSDK/Sources/TwidereAsset/Generated/Assets.swift @@ -35,13 +35,16 @@ public enum Asset { public static let trendingUp = ImageAsset(name: "Arrows/trending.up") } public enum Badge { - public static let circleMask = ImageAsset(name: "Badge/circle.mask") + public static let circleMask44 = ImageAsset(name: "Badge/circle.mask.44") + public static let circleMask88 = ImageAsset(name: "Badge/circle.mask.88") public static let circleMastodon = ImageAsset(name: "Badge/circle.mastodon") public static let circleTwitter = ImageAsset(name: "Badge/circle.twitter") public static let robot = ImageAsset(name: "Badge/robot") - public static let robotMask = ImageAsset(name: "Badge/robot.mask") + public static let robotMask44 = ImageAsset(name: "Badge/robot.mask.44") + public static let robotMask88 = ImageAsset(name: "Badge/robot.mask.88") public static let verified = ImageAsset(name: "Badge/verified") - public static let verifiedMask = ImageAsset(name: "Badge/verified.mask") + public static let verifiedMask44 = ImageAsset(name: "Badge/verified.mask.44") + public static let verifiedMask88 = ImageAsset(name: "Badge/verified.mask.88") } public enum Colors { public enum Banner { @@ -105,6 +108,7 @@ public enum Asset { public static let person2 = ImageAsset(name: "Human/person.2") public static let personExclamationMini = ImageAsset(name: "Human/person.exclamation.mini") public static let person = ImageAsset(name: "Human/person") + public static let personLarge = ImageAsset(name: "Human/person.large") public static let personMini = ImageAsset(name: "Human/person.mini") public static let personPlusMini = ImageAsset(name: "Human/person.plus.mini") } @@ -138,7 +142,9 @@ public enum Asset { } public enum ObjectTools { public static let bell = ImageAsset(name: "Object&Tools/bell") + 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 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") @@ -149,6 +155,7 @@ public enum Asset { public static let globeMini = ImageAsset(name: "Object&Tools/globe.mini") public static let globeMiniInline = ImageAsset(name: "Object&Tools/globe.mini.inline") public static let house = ImageAsset(name: "Object&Tools/house") + public static let houseLarge = ImageAsset(name: "Object&Tools/house.large") public static let icRoundRefresh = ImageAsset(name: "Object&Tools/ic.round.refresh") public static let lock = ImageAsset(name: "Object&Tools/lock") public static let lockMini = ImageAsset(name: "Object&Tools/lock.mini") @@ -156,6 +163,7 @@ public enum Asset { public static let lockOpen = ImageAsset(name: "Object&Tools/lock.open") public static let lockOpenMiniInline = ImageAsset(name: "Object&Tools/lock.open.mini.inline") public static let magnifyingglass = ImageAsset(name: "Object&Tools/magnifyingglass") + public static let magnifyingglassLarge = ImageAsset(name: "Object&Tools/magnifyingglass.large") public static let mappin = ImageAsset(name: "Object&Tools/mappin") public static let mappinMini = ImageAsset(name: "Object&Tools/mappin.mini") public static let note = ImageAsset(name: "Object&Tools/note") diff --git a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/Cell/Input/ComposeInputTableViewCell.swift b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/Cell/Input/ComposeInputTableViewCell.swift index 67a21e9e..36bb38aa 100644 --- a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/Cell/Input/ComposeInputTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/Cell/Input/ComposeInputTableViewCell.swift @@ -35,7 +35,7 @@ final public class ComposeInputTableViewCell: UITableViewCell { public let avatarView: ProfileAvatarView = { let imageView = ProfileAvatarView() - imageView.dimension = ComposeInputTableViewCell.avatarImageViewSize.width + imageView.setup(dimension: .inline) return imageView }() diff --git a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewController.swift b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewController.swift index 70068f92..478e85e7 100644 --- a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewController.swift +++ b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewController.swift @@ -283,7 +283,7 @@ extension ComposeContentViewController { guard let self = self else { return nil } guard isRequestLocation, let currentLocation = currentLocation else { return nil } - guard let authenticationContext = self.viewModel.configurationContext.authenticationService.activeAuthenticationContext.value, + guard let authenticationContext = self.viewModel.configurationContext.authenticationService.activeAuthenticationContext, case let .twitter(twitterAuthenticationContext) = authenticationContext else { return nil } diff --git a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel+Diffable.swift b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel+Diffable.swift index 1dd84f3d..d2f2d84c 100644 --- a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel+Diffable.swift +++ b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel+Diffable.swift @@ -173,11 +173,7 @@ extension ComposeContentViewModel { tableView: tableView, viewModel: ComposeReplyTableViewCell.ViewModel( status: status, - statusViewConfigureContext: StatusView.ConfigurationContext( - dateTimeProvider: configurationContext.dateTimeProvider, - twitterTextProvider: configurationContext.twitterTextProvider, - activeAuthenticationContext: Just(nil).eraseToAnyPublisher() - ) + statusViewConfigureContext: configurationContext.statusViewConfigureContext ) ) return composeReplyTableViewCell diff --git a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel.swift b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel.swift index 1ee377cf..38fae84e 100644 --- a/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel.swift +++ b/TwidereSDK/Sources/TwidereComposeUI/ComposeContent/ComposeContentViewModel.swift @@ -14,6 +14,7 @@ import CoreDataStack import TwitterSDK import MastodonSDK import TwidereCore +import TwidereUI import TwidereAsset import MetaTextKit import TwitterMeta @@ -207,7 +208,8 @@ public final class ComposeContentViewModel: NSObject { guard let author = author else { return } switch author { case .twitter: - let parseResult = configurationContext.twitterTextProvider.parse(text: currentTextInput) + let twitterTextProvider = configurationContext.statusViewConfigureContext.twitterTextProvider + let parseResult = twitterTextProvider.parse(text: currentTextInput) self.textInputLimitProgress = { guard parseResult.maxWeightedLength > 0 else { return .zero } return CGFloat(parseResult.weightedLength) / CGFloat(parseResult.maxWeightedLength) @@ -606,21 +608,18 @@ extension ComposeContentViewModel { public let apiService: APIService public let authenticationService: AuthenticationService public let mastodonEmojiService: MastodonEmojiService - public let dateTimeProvider: DateTimeProvider - public let twitterTextProvider: TwitterTextProvider - + public let statusViewConfigureContext: StatusView.ConfigurationContext + public init( apiService: APIService, authenticationService: AuthenticationService, mastodonEmojiService: MastodonEmojiService, - dateTimeProvider: DateTimeProvider, - twitterTextProvider: TwitterTextProvider + statusViewConfigureContext: StatusView.ConfigurationContext ) { self.apiService = apiService self.authenticationService = authenticationService self.mastodonEmojiService = mastodonEmojiService - self.dateTimeProvider = dateTimeProvider - self.twitterTextProvider = twitterTextProvider + self.statusViewConfigureContext = statusViewConfigureContext } } } @@ -653,7 +652,7 @@ extension ComposeContentViewModel { let metaContent = TwitterMetaContent.convert( content: content, urlMaximumLength: .max, - twitterTextProvider: configurationContext.twitterTextProvider + twitterTextProvider: configurationContext.statusViewConfigureContext.twitterTextProvider ) return metaContent diff --git a/TwidereSDK/Sources/TwidereComposeUI/MentionPick/MentionPickViewModel.swift b/TwidereSDK/Sources/TwidereComposeUI/MentionPick/MentionPickViewModel.swift index 179bd25c..fd4414b3 100644 --- a/TwidereSDK/Sources/TwidereComposeUI/MentionPick/MentionPickViewModel.swift +++ b/TwidereSDK/Sources/TwidereComposeUI/MentionPick/MentionPickViewModel.swift @@ -37,7 +37,7 @@ public final class MentionPickViewModel { self.primaryItem = primaryItem self.secondaryItems = secondaryItems - authenticationService.activeAuthenticationContext + authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } switch authenticationContext { diff --git a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterAuthentication.swift b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterAuthentication.swift index 74c4204d..f6e1a1b1 100644 --- a/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterAuthentication.swift +++ b/TwidereSDK/Sources/TwidereCore/Extension/CoreDataStack/TwitterAuthentication.swift @@ -12,6 +12,7 @@ import TwidereCommon import TwitterSDK extension AuthenticationIndex { + public var user: UserObject? { switch platform { case .twitter: diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift index 75436799..9d18b270 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusObject.swift @@ -55,6 +55,7 @@ extension StatusObject { public var attachments: [AttachmentObject] { switch self { case .twitter(let status): + let status = status.repost ?? status return status.attachments.map { .twitter($0) } case .mastodon(let status): return status.attachments.map { .mastodon($0) } diff --git a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift index b00d1dd0..a09fd54b 100644 --- a/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift +++ b/TwidereSDK/Sources/TwidereCore/Model/Status/StatusVisibility.swift @@ -8,6 +8,7 @@ import UIKit import MastodonSDK import TwidereAsset +import TwidereLocalization public enum StatusVisibility { case mastodon(Mastodon.Entity.Status.Visibility) @@ -32,3 +33,23 @@ extension StatusVisibility { } } } + +extension StatusVisibility { + public var accessibilityLabel: String? { + switch self { + case .mastodon(let visibility): + switch visibility { + case .public: + return L10n.Scene.Compose.Visibility.public + case .unlisted: + return L10n.Scene.Compose.Visibility.unlisted + case .private: + return L10n.Scene.Compose.Visibility.private + case .direct: + return L10n.Scene.Compose.Visibility.direct + case ._other: + return nil + } + } + } +} diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Extension/TwitterStatus+Property.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Extension/TwitterStatus+Property.swift index 5a2d87c8..cc02622c 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Extension/TwitterStatus+Property.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Extension/TwitterStatus+Property.swift @@ -23,6 +23,7 @@ extension TwitterStatus.Property { likeCount: entity.favoriteCount.flatMap(Int64.init) ?? 0, replyCount: 0, repostCount: entity.retweetCount.flatMap(Int64.init) ?? 0, + quoteCount: 0, source: entity.source.flatMap { source in do { let document = try HTMLDocument(string: source) @@ -128,6 +129,7 @@ extension TwitterStatus.Property { likeCount: status.publicMetrics.flatMap { Int64($0.likeCount) } ?? 0, replyCount: status.publicMetrics.flatMap { Int64($0.replyCount) } ?? 0, repostCount: status.publicMetrics.flatMap { Int64($0.retweetCount) } ?? 0, + quoteCount: status.publicMetrics.flatMap { Int64($0.quoteCount) } ?? 0, source: status.source, replyToStatusID: status.repliedToID, replyToUserID: status.inReplyToUserID, diff --git a/TwidereSDK/Sources/TwidereCore/Persistence/Persistence+TwitterStatus+V2.swift b/TwidereSDK/Sources/TwidereCore/Persistence/Persistence+TwitterStatus+V2.swift index c88fdf61..1cead545 100644 --- a/TwidereSDK/Sources/TwidereCore/Persistence/Persistence+TwitterStatus+V2.swift +++ b/TwidereSDK/Sources/TwidereCore/Persistence/Persistence+TwitterStatus+V2.swift @@ -226,12 +226,23 @@ extension Persistence.TwitterStatus { ) { status.update(entities: TwitterEntity(entity: context.entity.status.entities)) + // V2 only properties + // conversationID context.entity.status.conversationID.flatMap { status.update(conversationID: $0) } + // replyCount, quoteCount + context.entity.status.publicMetrics.flatMap { metrics in + status.update(replyCount: Int64(metrics.replyCount)) + status.update(replyCount: Int64(metrics.quoteCount)) + } + + // Not stable fields + // media context.dictionary.media(for: context.entity.status) .flatMap { media in let attachments = media.compactMap { $0.twitterAttachment } status.update(attachments: attachments) } + // place context.dictionary.place(for: context.entity.status) .flatMap { place in status.update(location: place.twitterLocation) diff --git a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Poll.swift b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Poll.swift index 9c8676f2..6f87c7ff 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Poll.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/APIService/APIService+Status+Poll.swift @@ -72,7 +72,7 @@ extension APIService { private struct MastodonVotePollContext { let pollID: Mastodon.Entity.Poll.ID - let choices: [Int] + // let choices: [Int] } public func voteMastodonStatusPoll( @@ -92,8 +92,7 @@ extension APIService { } let context = MastodonVotePollContext( - pollID: poll.id, - choices: choices + pollID: poll.id ) return context diff --git a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift index 2cc2f3bc..bb1278bb 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/AuthenticationService.swift @@ -29,7 +29,7 @@ public class AuthenticationService: NSObject { public let authenticationIndexes = CurrentValueSubject<[AuthenticationIndex], Never>([]) public let activeAuthenticationIndex = CurrentValueSubject(nil) - public let activeAuthenticationContext = CurrentValueSubject(nil) + @Published public var activeAuthenticationContext: AuthenticationContext? = nil @available(*, deprecated, message: "") public let activeTwitterAuthenticationBox = CurrentValueSubject(nil) @@ -108,8 +108,7 @@ public class AuthenticationService: NSObject { guard let authenticationContext = AuthenticationContext(authenticationIndex: authenticationIndex, appSecret: appSecret) else { return nil } return authenticationContext } - .assign(to: \.value, on: activeAuthenticationContext) - .store(in: &disposeBag) + .assign(to: &$activeAuthenticationContext) do { try authenticationIndexFetchedResultsController.performFetch() diff --git a/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift b/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift index c07f9226..d2027e72 100644 --- a/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift +++ b/TwidereSDK/Sources/TwidereCore/Service/ThemeService.swift @@ -39,11 +39,14 @@ public final class ThemeService { // set tab bar appearance let tabBarAppearance = UITabBarAppearance() tabBarAppearance.configureWithDefaultBackground() - let tabBarItemAppearance = UITabBarItemAppearance() -// tabBarItemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.clear] -// tabBarItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear] -// tabBarItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear] -// tabBarItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarAppearance.stackedLayoutAppearance = { + let tabBarItemAppearance = UITabBarItemAppearance() + tabBarItemAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.clear] + tabBarItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.clear] + return tabBarItemAppearance + }() UITabBar.appearance().standardAppearance = tabBarAppearance UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance // UITabBar.appearance().barTintColor = theme.tabBarBackgroundColor diff --git a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift index acb89357..3ee35a4d 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift +++ b/TwidereSDK/Sources/TwidereLocalization/Generated/Strings.swift @@ -36,19 +36,39 @@ public enum L10n { public static let twitter = L10n.tr("Localizable", "Accessibility.Common.Logo.Twitter") } public enum Status { + /// Author avatar + public static let authorAvatar = L10n.tr("Localizable", "Accessibility.Common.Status.AuthorAvatar") + /// Boosted + public static let boosted = L10n.tr("Localizable", "Accessibility.Common.Status.Boosted") + /// Content Warning + public static let contentWarning = L10n.tr("Localizable", "Accessibility.Common.Status.ContentWarning") + /// Liked + public static let liked = L10n.tr("Localizable", "Accessibility.Common.Status.Liked") /// Location public static let location = L10n.tr("Localizable", "Accessibility.Common.Status.Location") /// Media public static let media = L10n.tr("Localizable", "Accessibility.Common.Status.Media") + /// Poll option + public static let pollOptionOrdinalPrefix = L10n.tr("Localizable", "Accessibility.Common.Status.PollOptionOrdinalPrefix") /// Retweeted public static let retweeted = L10n.tr("Localizable", "Accessibility.Common.Status.Retweeted") public enum Actions { + /// Hide content + public static let hideContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideContent") + /// Hide media + public static let hideMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.HideMedia") /// Like public static let like = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Like") + /// Menu + public static let menu = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Menu") /// Reply public static let reply = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Reply") /// Retweet public static let retweet = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.Retweet") + /// Reveal content + public static let revealContent = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealContent") + /// Reveal media + public static let revealMedia = L10n.tr("Localizable", "Accessibility.Common.Status.Actions.RevealMedia") } } public enum Video { @@ -104,6 +124,10 @@ public enum L10n { public enum ManageAccounts { /// Add public static let add = L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.Add") + /// Current sign-in user: %@ + public static func currentSignInUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "Accessibility.Scene.ManageAccounts.CurrentSignInUser", String(describing: p1)) + } } public enum Search { /// History @@ -117,6 +141,12 @@ public enum L10n { public static let fontSize = L10n.tr("Localizable", "Accessibility.Scene.Settings.Display.FontSize") } } + public enum SignIn { + /// Please enter Mastodon domain to sign-in + public static let pleaseEnterMastodonDomainToSignIn = L10n.tr("Localizable", "Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn") + /// Twitter client authentication key setting + public static let twitterClientAuthenticationKeySetting = L10n.tr("Localizable", "Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting") + } public enum Timeline { /// Load public static let loadGap = L10n.tr("Localizable", "Accessibility.Scene.Timeline.LoadGap") @@ -136,6 +166,16 @@ public enum L10n { } } } + public enum VoiceOver { + /// Double tap and hold to display menu + public static let doubleTapAndHoldToDisplayMenu = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu") + /// Double tap and hold to open the accounts panel + public static let doubleTapAndHoldToOpenTheAccountsPanel = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel") + /// Double tap to open profile + public static let doubleTapToOpenProfile = L10n.tr("Localizable", "Accessibility.VoiceOver.DoubleTapToOpenProfile") + /// Selected + public static let selected = L10n.tr("Localizable", "Accessibility.VoiceOver.Selected") + } } public enum Common { @@ -574,6 +614,8 @@ public enum L10n { public enum Actions { /// Bookmark public static let bookmark = L10n.tr("Localizable", "Common.Controls.Status.Actions.Bookmark") + /// Boost + public static let boost = L10n.tr("Localizable", "Common.Controls.Status.Actions.Boost") /// Copy link public static let copyLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.CopyLink") /// Copy text @@ -588,6 +630,8 @@ public enum L10n { public static let retweet = L10n.tr("Localizable", "Common.Controls.Status.Actions.Retweet") /// Share public static let share = L10n.tr("Localizable", "Common.Controls.Status.Actions.Share") + /// Share content + public static let shareContent = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareContent") /// Share link public static let shareLink = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLink") /// Translate @@ -1395,14 +1439,52 @@ public enum L10n { } public enum Count { + /// Plural format key: "Input limit remains %#@character_count@" + public static func inputLimitRemains(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.input_limit_remains", p1) + } + /// Plural format key: "%#@like_count@" + public static func like(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.like", p1) + } + /// Plural format key: "%#@count_media@" + public static func media(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.media", p1) + } /// Plural format key: "%#@count_people@" public static func people(_ p1: Int) -> String { return L10n.tr("Localizable", "count.people", p1) } + /// Plural format key: "%#@post_count@" + public static func post(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.post", p1) + } + /// Plural format key: "%#@quote_count@" + public static func quote(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.quote", p1) + } + /// Plural format key: "%#@reblog_count@" + public static func reblog(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.reblog", p1) + } + /// Plural format key: "%#@reply_count@" + public static func reply(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.reply", p1) + } + /// Plural format key: "%#@retweet_count@" + public static func retweet(_ p1: Int) -> String { + return L10n.tr("Localizable", "count.retweet", p1) + } /// Plural format key: "%#@count_vote@" public static func vote(_ p1: Int) -> String { return L10n.tr("Localizable", "count.vote", p1) } + public enum MetricFormatted { + /// Plural format key: "%@ %#@post_count@" + public static func post(_ p1: Any, _ p2: Int) -> String { + return L10n.tr("Localizable", "count.metric_formatted.post", String(describing: p1), p2) + } + } public enum People { /// Plural format key: "%#@count_people_talking@" public static func talking(_ p1: Int) -> String { diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings index 5f157f4f..b3d6a65d 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "شعار تويتر"; "Accessibility.Common.More" = "المزيد"; "Accessibility.Common.NetworkImage" = "صورة الشبكة"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "أعجبني"; +"Accessibility.Common.Status.Actions.Menu" = "القائمة"; "Accessibility.Common.Status.Actions.Reply" = "رد"; "Accessibility.Common.Status.Actions.Retweet" = "أعد تغريده"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "الموقع"; "Accessibility.Common.Status.Media" = "الوسائط"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "أُعيد تغريده"; "Accessibility.Common.Video.Play" = "شغّل الفيديو"; "Accessibility.Scene.Compose.AddMention" = "أُذكر مستخدمًا"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "القائمة المنزلقة للحساب"; "Accessibility.Scene.Home.Menu" = "القائمة"; "Accessibility.Scene.ManageAccounts.Add" = "أضف"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "التأريخ"; "Accessibility.Scene.Search.Save" = "احفظ"; "Accessibility.Scene.Settings.Display.FontSize" = "حجم الخط"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "حمّل"; "Accessibility.Scene.User.Location" = "الموقع"; "Accessibility.Scene.User.Tab.Favourite" = "المفضلة"; "Accessibility.Scene.User.Tab.Media" = "الوسائط"; "Accessibility.Scene.User.Tab.Status" = "الحالات"; "Accessibility.Scene.User.Website" = "الموقع الإلكتروني"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "تويتر يعلق الحسابات التي تخالف %@"; "Common.Alerts.AccountSuspended.Title" = "الحساب معلق"; "Common.Alerts.AccountSuspended.TwitterRules" = "قواعد تويتر"; @@ -144,7 +161,7 @@ "Common.Controls.Friendship.Actions.Block" = "احجب"; "Common.Controls.Friendship.Actions.Blocked" = "محجوب"; "Common.Controls.Friendship.Actions.Follow" = "تابعه"; -"Common.Controls.Friendship.Actions.Following" = "يتابع"; +"Common.Controls.Friendship.Actions.Following" = "المتابَعون"; "Common.Controls.Friendship.Actions.Mute" = "اكتم"; "Common.Controls.Friendship.Actions.Pending" = "معلق"; "Common.Controls.Friendship.Actions.Report" = "أبلغ"; @@ -161,11 +178,12 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@ يتابعك"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@ لا يتابعك"; "Common.Controls.Ios.PhotoLibrary" = "مكتبة الصور"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "لا نتائج"; "Common.Controls.ProfileDashboard.Followers" = "المتابِعون"; -"Common.Controls.ProfileDashboard.Following" = "يتابع"; +"Common.Controls.ProfileDashboard.Following" = "المتابَعون"; "Common.Controls.ProfileDashboard.Listed" = "مدرج"; "Common.Controls.Status.Actions.Bookmark" = "أضفه للعلامات"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "انسخ الرابط"; "Common.Controls.Status.Actions.CopyText" = "انسخ النص"; "Common.Controls.Status.Actions.DeleteTweet" = "احذف التغريدة"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "اقتبس"; "Common.Controls.Status.Actions.Retweet" = "أعد تغريده"; "Common.Controls.Status.Actions.Share" = "شاركه"; +"Common.Controls.Status.Actions.ShareContent" = "شارك المحتوى"; "Common.Controls.Status.Actions.ShareLink" = "شارك الرابط"; "Common.Controls.Status.Actions.Translate" = "ترجمه"; "Common.Controls.Status.Actions.UnpinFromProfile" = "ألغ تثبيتها من ملفك الشخصي"; @@ -250,7 +269,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" = "احذف المسودة"; @@ -260,7 +279,7 @@ "Scene.Drawer.SignIn" = "لِج"; "Scene.Federated.Title" = "الشبكة الموحدة"; "Scene.Followers.Title" = "المتابِعون"; -"Scene.Following.Title" = "يتابع"; +"Scene.Following.Title" = "المتابَعون"; "Scene.Likes.Title" = "الإعجابات"; "Scene.Listed.Title" = "مدرج"; "Scene.Lists.Icons.Create" = "أنشئ قائمة"; @@ -364,7 +383,7 @@ "Scene.Settings.Display.SectionHeader.Media" = "الوسائط"; "Scene.Settings.Display.SectionHeader.Preview" = "معاينة"; "Scene.Settings.Display.SectionHeader.Text" = "النص"; -"Scene.Settings.Display.Text.AvatarStyle" = "نمط الصورة الرمزية"; +"Scene.Settings.Display.Text.AvatarStyle" = "شكل الصورة الرمزية"; "Scene.Settings.Display.Text.Circle" = "دائرة"; "Scene.Settings.Display.Text.RoundedSquare" = "مربع مدور الزوايا"; "Scene.Settings.Display.Text.UseTheSystemFontSize" = "استخدم حجم خط النظام"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict index cada4ede..3de26827 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ar.lproj/Localizable.stringsdict @@ -2,6 +2,222 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld media + one + 1 media + two + 1 وسائط + few + %ld media + many + %ld media + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld characters + one + 1 character + two + %ld characters + few + %ld characters + many + %ld characters + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + two + posts + few + posts + many + posts + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld posts + one + 1 post + two + %ld posts + few + %ld posts + many + %ld posts + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld quotes + one + 1 quote + two + %ld quotes + few + %ld quotes + many + %ld quotes + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld likes + one + 1 like + two + %ld likes + few + %ld likes + many + %ld likes + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld retweets + one + 1 retweet + two + %ld retweets + few + %ld retweets + many + %ld retweets + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld reblogs + one + 1 reblog + two + %ld reblogs + few + %ld reblogs + many + %ld reblogs + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + %ld replies + one + 1 reply + two + %ld replies + few + %ld replies + many + %ld replies + other + %ld replies + + count.vote NSStringLocalizedFormatKey @@ -15,15 +231,15 @@ zero %ld votes one - 1 vote + صوتٌ واحِد two - %ld votes + %ld صوتين few - %ld votes + %ld أصوات many - %ld votes + %ld صَوْتاً other - %ld votes + %ld صوت count.people @@ -37,17 +253,17 @@ NSStringFormatValueTypeKey ld zero - %ld people + %ld شخص one - 1 people + شخص واحد two - %ld people + شخصان %ld few - %ld people + %ld أشخاص many - %ld people + %ld شخصًا other - %ld people + %ld شخص count.people.talking @@ -63,15 +279,15 @@ zero %ld اشخاص يتحدثون one - %ld اشخاص يتحدثون + شخصٌ واحدٌ يتحدَّث two - %ld اشخاص يتحدثون + %ld شخصان يتحدثان few %ld اشخاص يتحدثون many - %ld اشخاص يتحدثون + %ld شخص يتحدَّثون other - %ld اشخاص يتحدثون + %ld شخص يتحدث date.year.left @@ -87,13 +303,13 @@ zero %ld سنوات متبقية one - %ld سنوات متبقية + تتبقى سنة two - %ld سنوات متبقية + %ld تتبقى سنتان few %ld سنوات متبقية many - %ld سنوات متبقية + %ld سنة متبقية other %ld سنوات متبقية @@ -111,15 +327,15 @@ zero %ld شهور متبقية one - %ld شهور متبقية + يتبقى شهر two - %ld شهور متبقية + %ld يتبقى شهران few %ld شهور متبقية many - %ld شهور متبقية + %ld شهر متبقية other - %ld شهور متبقية + %ld شهر متبقية date.day.left @@ -135,15 +351,15 @@ zero %ld أيام متبقية one - %ld أيام متبقية + يتبقى يوم two - %ld أيام متبقية + %ld يتبقى يومان few %ld أيام متبقية many - %ld أيام متبقية + تبقى %ld يوم other - %ld أيام متبقية + تبقى %ld يوم date.hour.left @@ -159,15 +375,15 @@ zero %ld ساعات متبقية one - %ld ساعات متبقية + تتبقى ساعة two - %ld ساعات متبقية + %ld تتبقى ساعتان few %ld ساعات متبقية many - %ld ساعات متبقية + %ld ساعة متبقية other - %ld ساعات متبقية + %ld ساعة متبقية date.minute.left @@ -183,15 +399,15 @@ zero %ld دقيقية متبقية one - %ld دقيقة متبقية + تتبقى دقيقة two - %ld دقائق متبقية + %ld تتبقى دقيقتان few %ld دقائق متبقية many - %ld دقائق متبقية + %ld دقيقة متبقية other - %ld دقائق متبقية + %ld دقيقة متبقية date.second.left @@ -207,15 +423,15 @@ zero %ld ثواني متبقية one - %ld ثانية متبقية + متبقي ثانية واحدة two - %ld ثواني متبقية + متبقي ثانيتان %ld few %ld ثواني متبقية many - %ld ثواني متبقية + %ld ثانية متبقية other - %ld ثواني متبقية + %ld ثانية متبقية diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings index 6a75978f..a39e9c19 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Logo de Twitter"; "Accessibility.Common.More" = "Més"; "Accessibility.Common.NetworkImage" = "Imatge de xarxa"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "M'agrada"; +"Accessibility.Common.Status.Actions.Menu" = "Menú"; "Accessibility.Common.Status.Actions.Reply" = "Resposta"; "Accessibility.Common.Status.Actions.Retweet" = "Repiular"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "Ubicació"; "Accessibility.Common.Status.Media" = "Multimèdia"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Repiulades"; "Accessibility.Common.Video.Play" = "Reprodueix el vídeo"; "Accessibility.Scene.Compose.AddMention" = "Afegeix menció"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Desplegable del compte"; "Accessibility.Scene.Home.Menu" = "Menú"; "Accessibility.Scene.ManageAccounts.Add" = "Afegir"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "Historial"; "Accessibility.Scene.Search.Save" = "Desa"; "Accessibility.Scene.Settings.Display.FontSize" = "Mida de la lletra"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "Carrega"; "Accessibility.Scene.User.Location" = "Ubicació"; "Accessibility.Scene.User.Tab.Favourite" = "Favorit"; "Accessibility.Scene.User.Tab.Media" = "Multimèdia"; "Accessibility.Scene.User.Tab.Status" = "Estats"; "Accessibility.Scene.User.Website" = "Lloc web"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "Twitter suspèn comptes que violen %@"; "Common.Alerts.AccountSuspended.Title" = "Compte suspès"; "Common.Alerts.AccountSuspended.TwitterRules" = "Normes de Twitter"; @@ -166,6 +183,7 @@ "Common.Controls.ProfileDashboard.Following" = "Seguint"; "Common.Controls.ProfileDashboard.Listed" = "Llistat"; "Common.Controls.Status.Actions.Bookmark" = "Marcador"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "Copia l'enllaç"; "Common.Controls.Status.Actions.CopyText" = "Copia el text"; "Common.Controls.Status.Actions.DeleteTweet" = "Esborra la piulada"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "Cita"; "Common.Controls.Status.Actions.Retweet" = "Repiular"; "Common.Controls.Status.Actions.Share" = "Share"; +"Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "Comparteix l'enllaç"; "Common.Controls.Status.Actions.Translate" = "Translate"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict index 6e26a2c3..e41b46c4 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ca.lproj/Localizable.stringsdict @@ -2,6 +2,150 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 quote + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 like + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 retweet + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings index f41c1beb..1bc3cc54 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Twitter logo"; "Accessibility.Common.More" = "Mehr"; "Accessibility.Common.NetworkImage" = "Netzwerkbild"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "Gefällt mir"; +"Accessibility.Common.Status.Actions.Menu" = "Menü"; "Accessibility.Common.Status.Actions.Reply" = "Antworten"; "Accessibility.Common.Status.Actions.Retweet" = "Retweet"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "Standort"; "Accessibility.Common.Status.Media" = "Medien"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Retweetet"; "Accessibility.Common.Video.Play" = "Video abspielen"; "Accessibility.Scene.Compose.AddMention" = "Add mention"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Konto DropDown-Liste"; "Accessibility.Scene.Home.Menu" = "Menü"; "Accessibility.Scene.ManageAccounts.Add" = "Hinzufügen"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "Verlauf"; "Accessibility.Scene.Search.Save" = "Speichern"; "Accessibility.Scene.Settings.Display.FontSize" = "Schriftgröße"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "Laden"; "Accessibility.Scene.User.Location" = "Standort"; "Accessibility.Scene.User.Tab.Favourite" = "Favoriten"; "Accessibility.Scene.User.Tab.Media" = "Medien"; "Accessibility.Scene.User.Tab.Status" = "Status"; "Accessibility.Scene.User.Website" = "Webseite"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "Twitter sperrt Konten die gegen %@ verstoßen"; "Common.Alerts.AccountSuspended.Title" = "Konto gesperrt"; "Common.Alerts.AccountSuspended.TwitterRules" = "Twitter-Regeln"; @@ -166,6 +183,7 @@ "Common.Controls.ProfileDashboard.Following" = "Folgen"; "Common.Controls.ProfileDashboard.Listed" = "Listet"; "Common.Controls.Status.Actions.Bookmark" = "Lesezeichen"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "Link kopieren"; "Common.Controls.Status.Actions.CopyText" = "Text kopieren"; "Common.Controls.Status.Actions.DeleteTweet" = "Tweet löschen"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "Zitieren"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.Share" = "Share"; +"Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "Link teilen"; "Common.Controls.Status.Actions.Translate" = "Translate"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict index 6e26a2c3..e41b46c4 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/de.lproj/Localizable.stringsdict @@ -2,6 +2,150 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 quote + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 like + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 retweet + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings index e80aa655..0e323562 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Twitter Logo"; "Accessibility.Common.More" = "More"; "Accessibility.Common.NetworkImage" = "Network Image"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "Like"; +"Accessibility.Common.Status.Actions.Menu" = "Menu"; "Accessibility.Common.Status.Actions.Reply" = "Reply"; "Accessibility.Common.Status.Actions.Retweet" = "Retweet"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "Location"; "Accessibility.Common.Status.Media" = "Media"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Retweeted"; "Accessibility.Common.Video.Play" = "Play video"; "Accessibility.Scene.Compose.AddMention" = "Add mention"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Account DropDown"; "Accessibility.Scene.Home.Menu" = "Menu"; "Accessibility.Scene.ManageAccounts.Add" = "Add"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "History"; "Accessibility.Scene.Search.Save" = "Save"; "Accessibility.Scene.Settings.Display.FontSize" = "Font Size"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "Load"; "Accessibility.Scene.User.Location" = "Location"; "Accessibility.Scene.User.Tab.Favourite" = "Favourite"; "Accessibility.Scene.User.Tab.Media" = "Media"; "Accessibility.Scene.User.Tab.Status" = "Statuses"; "Accessibility.Scene.User.Website" = "Website"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "Twitter suspends accounts which violate the %@"; "Common.Alerts.AccountSuspended.Title" = "Account Suspended"; "Common.Alerts.AccountSuspended.TwitterRules" = "Twitter Rules"; @@ -166,6 +183,7 @@ "Common.Controls.ProfileDashboard.Following" = "Following"; "Common.Controls.ProfileDashboard.Listed" = "Listed"; "Common.Controls.Status.Actions.Bookmark" = "Bookmark"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "Copy link"; "Common.Controls.Status.Actions.CopyText" = "Copy text"; "Common.Controls.Status.Actions.DeleteTweet" = "Delete tweet"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "Quote"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.Share" = "Share"; +"Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "Share link"; "Common.Controls.Status.Actions.Translate" = "Translate"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict index 6e26a2c3..e41b46c4 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/en.lproj/Localizable.stringsdict @@ -2,6 +2,150 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 quote + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 like + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 retweet + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings index 8b4cacf9..fe71b5aa 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Logo de Twitter"; "Accessibility.Common.More" = "Más"; "Accessibility.Common.NetworkImage" = "Imagen de red"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "Me gusta"; +"Accessibility.Common.Status.Actions.Menu" = "Menú"; "Accessibility.Common.Status.Actions.Reply" = "Responder"; "Accessibility.Common.Status.Actions.Retweet" = "Retwittear"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "Ubicación"; "Accessibility.Common.Status.Media" = "Multimedia"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Retwitteados"; "Accessibility.Common.Video.Play" = "Reproducir vídeo"; "Accessibility.Scene.Compose.AddMention" = "Añadir mención"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Desplegable de la cuenta"; "Accessibility.Scene.Home.Menu" = "Menú"; "Accessibility.Scene.ManageAccounts.Add" = "Añadir"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "Historial"; "Accessibility.Scene.Search.Save" = "Guardar"; "Accessibility.Scene.Settings.Display.FontSize" = "Tamaño de fuente"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "Cargar"; "Accessibility.Scene.User.Location" = "Ubicación"; "Accessibility.Scene.User.Tab.Favourite" = "Favorito"; "Accessibility.Scene.User.Tab.Media" = "Multimedia"; "Accessibility.Scene.User.Tab.Status" = "Estados"; "Accessibility.Scene.User.Website" = "Página web"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "Twitter suspende cuentas que violan %@"; "Common.Alerts.AccountSuspended.Title" = "Cuenta suspendida"; "Common.Alerts.AccountSuspended.TwitterRules" = "Reglas de Twitter"; @@ -87,7 +104,7 @@ "Common.Alerts.MediaSaveFail.Title" = "Error al guardar archivo multimedia"; "Common.Alerts.MediaSaved.Title" = "Archivo multimedia guardado"; "Common.Alerts.MediaSaving.Title" = "Guardando multimedia"; -"Common.Alerts.MediaSharing.Title" = "Los medios serán compartidos una vez completada la descarga"; +"Common.Alerts.MediaSharing.Title" = "Los archivos multimedia serán compartidos una vez completada la descarga"; "Common.Alerts.MuteUserConfirm.Title" = "¿Desea silenciar a %@?"; "Common.Alerts.MuteUserSuccess.Title" = "%@ ha sido silenciado"; "Common.Alerts.NoTweetsFound.Title" = "No se encontraron Tweets"; @@ -136,7 +153,7 @@ "Common.Controls.Actions.Remove" = "Eliminar"; "Common.Controls.Actions.Save" = "Guardar"; "Common.Controls.Actions.SavePhoto" = "Guardar foto"; -"Common.Controls.Actions.ShareMedia" = "Compartir medios"; +"Common.Controls.Actions.ShareMedia" = "Compartir multimedia"; "Common.Controls.Actions.SignIn" = "Iniciar sesión"; "Common.Controls.Actions.SignOut" = "Cerrar sesión"; "Common.Controls.Actions.TakePhoto" = "Hacer una foto"; @@ -161,11 +178,12 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@ te sigue"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@ no te sigue"; "Common.Controls.Ios.PhotoLibrary" = "Galería de fotos"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "Sin resultados"; "Common.Controls.ProfileDashboard.Followers" = "Seguidores"; "Common.Controls.ProfileDashboard.Following" = "Siguiendo"; "Common.Controls.ProfileDashboard.Listed" = "Listado"; "Common.Controls.Status.Actions.Bookmark" = "Marcador"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "Copiar enlace"; "Common.Controls.Status.Actions.CopyText" = "Copiar texto"; "Common.Controls.Status.Actions.DeleteTweet" = "Eliminar tweet"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "Cita"; "Common.Controls.Status.Actions.Retweet" = "Retwittear"; "Common.Controls.Status.Actions.Share" = "Compartir"; +"Common.Controls.Status.Actions.ShareContent" = "Compartir contenido"; "Common.Controls.Status.Actions.ShareLink" = "Compartir enlace"; "Common.Controls.Status.Actions.Translate" = "Traducir"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Desfijar del perfil"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict index 95479f4a..b4800dbb 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/es.lproj/Localizable.stringsdict @@ -2,6 +2,150 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 archivo multimedia + other + %ld archivos multimedia + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 quote + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 like + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 retweet + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings index 23fc3521..c0a36a57 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.strings @@ -3,16 +3,26 @@ "Accessibility.Common.Done" = "完了"; "Accessibility.Common.Logo.Github" = "GitHub ロゴ"; "Accessibility.Common.Logo.Mastodon" = "Mastodon ロゴ"; -"Accessibility.Common.Logo.Telegram" = "Telegram Logo"; +"Accessibility.Common.Logo.Telegram" = "Telegram ロゴ"; "Accessibility.Common.Logo.Twidere" = "Twidere X ロゴ"; "Accessibility.Common.Logo.Twitter" = "Twitter ロゴ"; "Accessibility.Common.More" = "もっと見る"; -"Accessibility.Common.NetworkImage" = "Network Image"; +"Accessibility.Common.NetworkImage" = "ネットワーク画像"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "Like"; +"Accessibility.Common.Status.Actions.Menu" = "メニュー"; "Accessibility.Common.Status.Actions.Reply" = "返信"; -"Accessibility.Common.Status.Actions.Retweet" = "Retweet"; +"Accessibility.Common.Status.Actions.Retweet" = "リツイート"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "位置情報"; "Accessibility.Common.Status.Media" = "メディア"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Retweeted"; "Accessibility.Common.Video.Play" = "動画の再生"; "Accessibility.Scene.Compose.AddMention" = "Add mention"; @@ -32,31 +42,38 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "アカウント ドロップダウン"; "Accessibility.Scene.Home.Menu" = "メニュー"; "Accessibility.Scene.ManageAccounts.Add" = "追加"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "履歴"; "Accessibility.Scene.Search.Save" = "保存"; "Accessibility.Scene.Settings.Display.FontSize" = "フォントサイズ"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "読み込み"; "Accessibility.Scene.User.Location" = "位置情報"; "Accessibility.Scene.User.Tab.Favourite" = "お気に入り"; "Accessibility.Scene.User.Tab.Media" = "メディア"; "Accessibility.Scene.User.Tab.Status" = "投稿"; "Accessibility.Scene.User.Website" = "ウェブサイト"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "Twitterは%@に違反したアカウントを凍結します"; "Common.Alerts.AccountSuspended.Title" = "アカウントは凍結されています"; "Common.Alerts.AccountSuspended.TwitterRules" = "Twitterルール"; "Common.Alerts.AccountTemporarilyLocked.Message" = "Twitter を開いてロックを解除する"; "Common.Alerts.AccountTemporarilyLocked.Title" = "アカウントは一時的にロックされています"; -"Common.Alerts.BlockUserConfirm.Title" = "Do you want to block %@?"; +"Common.Alerts.BlockUserConfirm.Title" = "%@ をブロックしますか?"; "Common.Alerts.BlockUserSuccess.Title" = "%@ はブロックされました"; "Common.Alerts.CancelFollowRequest.Message" = "%@ のフォローリクエストをキャンセルしますか?"; -"Common.Alerts.DeleteTootConfirm.Message" = "Do you want to delete this toot?"; -"Common.Alerts.DeleteTootConfirm.Title" = "Delete Toot"; -"Common.Alerts.DeleteTweetConfirm.Message" = "Do you want to delete this tweet?"; -"Common.Alerts.DeleteTweetConfirm.Title" = "Delete Tweet"; +"Common.Alerts.DeleteTootConfirm.Message" = "このトゥートを削除しますか?"; +"Common.Alerts.DeleteTootConfirm.Title" = "トゥートを削除"; +"Common.Alerts.DeleteTweetConfirm.Message" = "このツイートを削除しますか?"; +"Common.Alerts.DeleteTweetConfirm.Title" = "ツイートを削除"; "Common.Alerts.FailedToBlockUser.Message" = "もう一度やり直してください"; "Common.Alerts.FailedToBlockUser.Title" = "%@ のブロックに失敗"; "Common.Alerts.FailedToDeleteToot.Message" = "もう一度やり直してください"; -"Common.Alerts.FailedToDeleteToot.Title" = "Failed to Delete Toot"; +"Common.Alerts.FailedToDeleteToot.Title" = "トゥートの削除に失敗しました"; "Common.Alerts.FailedToDeleteTweet.Message" = "もう一度やり直してください"; "Common.Alerts.FailedToDeleteTweet.Title" = "ツイートの削除に失敗しました"; "Common.Alerts.FailedToFollowing.Message" = "もう一度やり直してください"; @@ -87,8 +104,8 @@ "Common.Alerts.MediaSaveFail.Title" = "メディアを保存できませんでした"; "Common.Alerts.MediaSaved.Title" = "メディアを保存しました"; "Common.Alerts.MediaSaving.Title" = "メディアの保存"; -"Common.Alerts.MediaSharing.Title" = "Media will be shared after download is completed"; -"Common.Alerts.MuteUserConfirm.Title" = "Do you want to mute %@?"; +"Common.Alerts.MediaSharing.Title" = "メディアはダウンロードが完了したら共有されます"; +"Common.Alerts.MuteUserConfirm.Title" = "%@ をミュートしますか?"; "Common.Alerts.MuteUserSuccess.Title" = "%@ はミュートされました"; "Common.Alerts.NoTweetsFound.Title" = "ツイートが見つかりませんでした"; "Common.Alerts.PermissionDeniedFriendshipBlocked.Message" = "ユーザーのリクエストにより、このアカウントのフォローをブロックされています"; @@ -102,33 +119,33 @@ "Common.Alerts.RateLimitExceeded.Title" = "レート制限を超えました"; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@ はスパムとして報告されブロックされました"; "Common.Alerts.ReportUserSuccess.Title" = "%@ はスパムとして報告されました"; -"Common.Alerts.SignOutUserConfirm.Message" = "Do you want to sign out?"; -"Common.Alerts.SignOutUserConfirm.Title" = "Sign out"; +"Common.Alerts.SignOutUserConfirm.Message" = "サインアウトしますか?"; +"Common.Alerts.SignOutUserConfirm.Title" = "サインアウト"; "Common.Alerts.TooManyRequests.Title" = "リクエストが多すぎます"; -"Common.Alerts.TootDeleted.Title" = "Toot Deleted"; -"Common.Alerts.TootFail.DraftSavedMessage" = "Your toot has been saved to Drafts."; +"Common.Alerts.TootDeleted.Title" = "トゥートを削除しました"; +"Common.Alerts.TootFail.DraftSavedMessage" = "あなたのトゥートは下書きに保存されました。"; "Common.Alerts.TootFail.Message" = "もう一度やり直してください"; -"Common.Alerts.TootFail.Title" = "Failed to Toot"; -"Common.Alerts.TootPosted.Title" = "Toot Posted"; -"Common.Alerts.TootSending.Title" = "Sending toot"; +"Common.Alerts.TootFail.Title" = "トゥートに失敗しました"; +"Common.Alerts.TootPosted.Title" = "トゥートを投稿しました"; +"Common.Alerts.TootSending.Title" = "トゥートを送信中"; "Common.Alerts.TweetDeleted.Title" = "ツイートを削除しました"; -"Common.Alerts.TweetFail.DraftSavedMessage" = "Your tweet has been saved to Drafts."; +"Common.Alerts.TweetFail.DraftSavedMessage" = "あなたのツイートは下書きに保存されました。"; "Common.Alerts.TweetFail.Message" = "もう一度やり直してください"; "Common.Alerts.TweetFail.Title" = "ツイートに失敗しました"; -"Common.Alerts.TweetPosted.Title" = "Tweet Posted"; +"Common.Alerts.TweetPosted.Title" = "ツイートを投稿しました"; "Common.Alerts.TweetSending.Title" = "ツイートを送信中"; "Common.Alerts.TweetSent.Title" = "ツイートを送信しました"; -"Common.Alerts.UnblockUserConfirm.Title" = "Do you want to unblock %@?"; +"Common.Alerts.UnblockUserConfirm.Title" = "%@ をブロック解除しますか?"; "Common.Alerts.UnblockUserSuccess.Title" = "%@ はブロック解除されました"; "Common.Alerts.UnfollowUser.Message" = "%@ のフォローを解除しますか?"; "Common.Alerts.UnfollowingSuccess.Title" = "フォロー解除に成功しました"; -"Common.Alerts.UnmuteUserConfirm.Title" = "Do you want to unmute %@?"; +"Common.Alerts.UnmuteUserConfirm.Title" = "%@ をミュート解除しますか?"; "Common.Alerts.UnmuteUserSuccess.Title" = "%@ はミュート解除されました"; "Common.Controls.Actions.Add" = "追加"; "Common.Controls.Actions.Browse" = "Browse"; "Common.Controls.Actions.Cancel" = "キャンセル"; "Common.Controls.Actions.Confirm" = "確認"; -"Common.Controls.Actions.Delete" = "Delete"; +"Common.Controls.Actions.Delete" = "削除"; "Common.Controls.Actions.Edit" = "編集"; "Common.Controls.Actions.Ok" = "OK"; "Common.Controls.Actions.OpenInSafari" = "Safari で開く"; @@ -138,7 +155,7 @@ "Common.Controls.Actions.SavePhoto" = "写真を保存"; "Common.Controls.Actions.ShareMedia" = "メディアを共有"; "Common.Controls.Actions.SignIn" = "サインイン"; -"Common.Controls.Actions.SignOut" = "Sign out"; +"Common.Controls.Actions.SignOut" = "サインアウト"; "Common.Controls.Actions.TakePhoto" = "写真を撮影"; "Common.Controls.Actions.Yes" = "はい"; "Common.Controls.Friendship.Actions.Block" = "ブロック"; @@ -149,7 +166,7 @@ "Common.Controls.Friendship.Actions.Pending" = "保留中"; "Common.Controls.Friendship.Actions.Report" = "報告"; "Common.Controls.Friendship.Actions.ReportAndBlock" = "報告とブロック"; -"Common.Controls.Friendship.Actions.Request" = "Request"; +"Common.Controls.Friendship.Actions.Request" = "リクエスト"; "Common.Controls.Friendship.Actions.Unblock" = "ブロック解除"; "Common.Controls.Friendship.Actions.Unfollow" = "フォロー解除"; "Common.Controls.Friendship.Actions.Unmute" = "ミュート解除"; @@ -166,59 +183,61 @@ "Common.Controls.ProfileDashboard.Following" = "フォロー中"; "Common.Controls.ProfileDashboard.Listed" = "リスト"; "Common.Controls.Status.Actions.Bookmark" = "ブックマーク"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "リンクをコピー"; "Common.Controls.Status.Actions.CopyText" = "テキストをコピー"; "Common.Controls.Status.Actions.DeleteTweet" = "ツイートを削除"; -"Common.Controls.Status.Actions.PinOnProfile" = "Pin on Profile"; +"Common.Controls.Status.Actions.PinOnProfile" = "プロフィールに固定表示"; "Common.Controls.Status.Actions.Quote" = "引用"; -"Common.Controls.Status.Actions.Retweet" = "Retweet"; -"Common.Controls.Status.Actions.Share" = "Share"; +"Common.Controls.Status.Actions.Retweet" = "リツイート"; +"Common.Controls.Status.Actions.Share" = "共有"; +"Common.Controls.Status.Actions.ShareContent" = "コンテンツを共有"; "Common.Controls.Status.Actions.ShareLink" = "リンクを共有"; -"Common.Controls.Status.Actions.Translate" = "Translate"; -"Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; -"Common.Controls.Status.Actions.Vote" = "Vote"; +"Common.Controls.Status.Actions.Translate" = "翻訳"; +"Common.Controls.Status.Actions.UnpinFromProfile" = "プロフィールへの固定を解除"; +"Common.Controls.Status.Actions.Vote" = "投票"; "Common.Controls.Status.Media" = "メディア"; "Common.Controls.Status.Poll.Expired" = "Closed"; -"Common.Controls.Status.Poll.TotalPeople" = "%@ people"; -"Common.Controls.Status.Poll.TotalPerson" = "%@ person"; -"Common.Controls.Status.Poll.TotalVote" = "%@ vote"; -"Common.Controls.Status.Poll.TotalVotes" = "%@ votes"; -"Common.Controls.Status.Thread.Show" = "Show this thread"; +"Common.Controls.Status.Poll.TotalPeople" = "%@ 人"; +"Common.Controls.Status.Poll.TotalPerson" = "%@ 人"; +"Common.Controls.Status.Poll.TotalVote" = "%@ 票"; +"Common.Controls.Status.Poll.TotalVotes" = "%@ 票"; +"Common.Controls.Status.Thread.Show" = "このスレッドを表示"; "Common.Controls.Status.UserBoosted" = "%@ boosted"; "Common.Controls.Status.UserRetweeted" = "%@ がリツイート"; "Common.Controls.Status.YouBoosted" = "You boosted"; -"Common.Controls.Status.YouRetweeted" = "You retweeted"; +"Common.Controls.Status.YouRetweeted" = "あなたがリツイート"; "Common.Controls.Timeline.LoadMore" = "さらに読み込む"; -"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" = "%@ いいね"; +"Common.Countable.Like.Single" = "%@ いいね"; +"Common.Countable.List.Multiple" = "%@ リスト"; +"Common.Countable.List.Single" = "%@ リスト"; +"Common.Countable.Member.Multiple" = "%@ メンバー"; +"Common.Countable.Member.Single" = "%@ メンバー"; +"Common.Countable.Photo.Multiple" = "%@ 枚の写真"; +"Common.Countable.Photo.Single" = "%@ 枚の写真"; +"Common.Countable.Quote.Mutiple" = "%@ 引用"; +"Common.Countable.Quote.Single" = "%@ 引用"; +"Common.Countable.Reply.Mutiple" = "%@ 返信"; +"Common.Countable.Reply.Single" = "%@ 返信"; +"Common.Countable.Retweet.Mutiple" = "%@ リツイート"; +"Common.Countable.Retweet.Single" = "%@ リツイート"; +"Common.Countable.Tweet.Multiple" = "%@ ツイート"; +"Common.Countable.Tweet.Single" = "%@ ツイート"; "Common.Notification.Favourite" = "%@ さんがトゥートをお気に入りにしました"; -"Common.Notification.Follow" = "%@ followed you"; -"Common.Notification.FollowRequest" = "%@ has requested to follow you"; -"Common.Notification.Mentions" = "%@ mentions you"; -"Common.Notification.Messages.Content" = "%@ sent you a message"; -"Common.Notification.Messages.Title" = "New direct message"; +"Common.Notification.Follow" = "%@ がフォローしました"; +"Common.Notification.FollowRequest" = "%@ がフォローリクエストしました"; +"Common.Notification.Mentions" = "%@ がメンションしました"; +"Common.Notification.Messages.Content" = "%@ がメッセージを送信しました"; +"Common.Notification.Messages.Title" = "新しいダイレクト メッセージ"; "Common.Notification.OwnPoll" = "アンケートは終了しました"; "Common.Notification.Poll" = "投票したアンケートが締め切られました"; "Common.Notification.Reblog" = "%@ さんがトゥートをブーストしました"; -"Common.Notification.Status" = "%@ just posted"; +"Common.Notification.Status" = "%@ が投稿しました"; "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.ContentMessages.Description" = "ダイレクトメッセージ"; "Common.NotificationChannel.ContentMessages.Name" = "メッセージ"; "Scene.Authentication.Title" = "認証"; "Scene.Bookmark.Title" = "ブックマーク"; @@ -385,17 +404,17 @@ "Scene.Settings.Misc.Nitter.Input.Placeholder" = "Nitter Instance"; "Scene.Settings.Misc.Nitter.Input.Value" = "Instance URL"; "Scene.Settings.Misc.Nitter.Title" = "Third-party Twitter data provider"; -"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"; -"Scene.Settings.Misc.Proxy.Port.Error" = "Proxy server port must be numbers"; -"Scene.Settings.Misc.Proxy.Port.Title" = "Port"; -"Scene.Settings.Misc.Proxy.Server" = "Server"; -"Scene.Settings.Misc.Proxy.Title" = "Proxy settings"; +"Scene.Settings.Misc.Proxy.Enable.Description" = "すべてのネットワーク要求にプロキシを使用する"; +"Scene.Settings.Misc.Proxy.Enable.Title" = "プロキシ"; +"Scene.Settings.Misc.Proxy.Password" = "パスワード"; +"Scene.Settings.Misc.Proxy.Port.Error" = "プロキシサーバーのポートは番号でなければなりません"; +"Scene.Settings.Misc.Proxy.Port.Title" = "ポート"; +"Scene.Settings.Misc.Proxy.Server" = "サーバー"; +"Scene.Settings.Misc.Proxy.Title" = "プロキシ設定"; "Scene.Settings.Misc.Proxy.Type.Http" = "HTTP"; -"Scene.Settings.Misc.Proxy.Type.Reverse" = "Reverse"; -"Scene.Settings.Misc.Proxy.Type.Title" = "Proxy type"; -"Scene.Settings.Misc.Proxy.Username" = "Username"; +"Scene.Settings.Misc.Proxy.Type.Reverse" = "リバース"; +"Scene.Settings.Misc.Proxy.Type.Title" = "プロキシの種類"; +"Scene.Settings.Misc.Proxy.Username" = "ユーザー名"; "Scene.Settings.Misc.Title" = "その他"; "Scene.Settings.Notification.Accounts" = "アカウント"; "Scene.Settings.Notification.NotificationSwitch" = "通知を表示"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict index 5c725633..7191bb92 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ja.lproj/Localizable.stringsdict @@ -2,6 +2,132 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld replies + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings index 233f6dee..d909f126 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "트위터 로고"; "Accessibility.Common.More" = "더 보기"; "Accessibility.Common.NetworkImage" = "네트워크 이미지"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "좋아요"; +"Accessibility.Common.Status.Actions.Menu" = "메뉴"; "Accessibility.Common.Status.Actions.Reply" = "답글"; "Accessibility.Common.Status.Actions.Retweet" = "리트윗"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Author avatar"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "위치"; "Accessibility.Common.Status.Media" = "미디어"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "리트윗함"; "Accessibility.Common.Video.Play" = "영상 보기"; "Accessibility.Scene.Compose.AddMention" = "답글 달기"; @@ -20,43 +30,50 @@ "Accessibility.Scene.Compose.Image" = "사진 넣기"; "Accessibility.Scene.Compose.Location.Disable" = "위치 끄기"; "Accessibility.Scene.Compose.Location.Enable" = "위치 켜기"; -"Accessibility.Scene.Compose.MediaInsert.Camera" = "Take Photo"; -"Accessibility.Scene.Compose.MediaInsert.Gif" = "Add GIF"; -"Accessibility.Scene.Compose.MediaInsert.Library" = "Browse Library"; -"Accessibility.Scene.Compose.MediaInsert.RecordVideo" = "Record Video"; +"Accessibility.Scene.Compose.MediaInsert.Camera" = "사진 찍기"; +"Accessibility.Scene.Compose.MediaInsert.Gif" = "움짤 넣기"; +"Accessibility.Scene.Compose.MediaInsert.Library" = "라이브러리 탐색"; +"Accessibility.Scene.Compose.MediaInsert.RecordVideo" = "영상 찍기"; "Accessibility.Scene.Compose.Send" = "보내기"; "Accessibility.Scene.Compose.Thread" = "타래 모드"; -"Accessibility.Scene.Gif.Search" = "Search GIF"; +"Accessibility.Scene.Gif.Search" = "움짤 찾기"; "Accessibility.Scene.Gif.Title" = "GIPHY"; "Accessibility.Scene.Home.Compose" = "쓰기"; "Accessibility.Scene.Home.Drawer.AccountDropdown" = "계정 목록"; "Accessibility.Scene.Home.Menu" = "메뉴"; "Accessibility.Scene.ManageAccounts.Add" = "새로 만들기"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Current sign-in user: %@"; "Accessibility.Scene.Search.History" = "기록"; "Accessibility.Scene.Search.Save" = "저장"; "Accessibility.Scene.Settings.Display.FontSize" = "글꼴 크기"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Please enter Mastodon domain to sign-in"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter client authentication key setting"; "Accessibility.Scene.Timeline.LoadGap" = "불러오기"; "Accessibility.Scene.User.Location" = "위치"; "Accessibility.Scene.User.Tab.Favourite" = "좋아요"; "Accessibility.Scene.User.Tab.Media" = "미디어"; "Accessibility.Scene.User.Tab.Status" = "상태"; "Accessibility.Scene.User.Website" = "웹사이트"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Double tap and hold to display menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Double tap and hold to open the accounts panel"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Double tap to open profile"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "트위터는 %@ 규정을 어기면 계정을 정지시킵니다."; "Common.Alerts.AccountSuspended.Title" = "계정 정지됨"; "Common.Alerts.AccountSuspended.TwitterRules" = "트위터 규정"; "Common.Alerts.AccountTemporarilyLocked.Message" = "트위터를 열어 잠금 풀기"; "Common.Alerts.AccountTemporarilyLocked.Title" = "계정이 일시적으로 잠겼습니다."; -"Common.Alerts.BlockUserConfirm.Title" = "Do you want to block %@?"; +"Common.Alerts.BlockUserConfirm.Title" = "%@를 차단할까요?"; "Common.Alerts.BlockUserSuccess.Title" = "%@를 차단했습니다."; "Common.Alerts.CancelFollowRequest.Message" = "%@의 팔로우 요청을 취소할까요?"; -"Common.Alerts.DeleteTootConfirm.Message" = "Do you want to delete this toot?"; -"Common.Alerts.DeleteTootConfirm.Title" = "Delete Toot"; -"Common.Alerts.DeleteTweetConfirm.Message" = "Do you want to delete this tweet?"; -"Common.Alerts.DeleteTweetConfirm.Title" = "Delete Tweet"; +"Common.Alerts.DeleteTootConfirm.Message" = "이 툿을 지울까요?"; +"Common.Alerts.DeleteTootConfirm.Title" = "툿을 지웁니다."; +"Common.Alerts.DeleteTweetConfirm.Message" = "이 트윗을 지울까요?"; +"Common.Alerts.DeleteTweetConfirm.Title" = "트윗을 지웁니다."; "Common.Alerts.FailedToBlockUser.Message" = "다시 해보세요."; "Common.Alerts.FailedToBlockUser.Title" = "%@를 차단하지 못했습니다."; "Common.Alerts.FailedToDeleteToot.Message" = "다시 해보세요."; -"Common.Alerts.FailedToDeleteToot.Title" = "Failed to Delete Toot"; +"Common.Alerts.FailedToDeleteToot.Title" = "툿을 지우지 못했습니다."; "Common.Alerts.FailedToDeleteTweet.Message" = "다시 해보세요."; "Common.Alerts.FailedToDeleteTweet.Title" = "트윗을 지우지 못했습니다."; "Common.Alerts.FailedToFollowing.Message" = "다시 해보세요."; @@ -88,7 +105,7 @@ "Common.Alerts.MediaSaved.Title" = "저장했습니다"; "Common.Alerts.MediaSaving.Title" = "저장하기"; "Common.Alerts.MediaSharing.Title" = "다운로드 뒤 미디어가 공유됩니다"; -"Common.Alerts.MuteUserConfirm.Title" = "Do you want to mute %@?"; +"Common.Alerts.MuteUserConfirm.Title" = "%@를 뮤트할까요?"; "Common.Alerts.MuteUserSuccess.Title" = "%@를 뮤트했습니다."; "Common.Alerts.NoTweetsFound.Title" = "트윗을 찾을 수 없습니다."; "Common.Alerts.PermissionDeniedFriendshipBlocked.Message" = "이 사용자의 요청으로 이 계정을 팔로우하는 것이 차단되어 있습니다."; @@ -102,33 +119,33 @@ "Common.Alerts.RateLimitExceeded.Title" = "한도를 넘어서 제한이 걸렸습니다."; "Common.Alerts.ReportAndBlockUserSuccess.Title" = "%@는 스팸으로 신고받아 차단되었습니다."; "Common.Alerts.ReportUserSuccess.Title" = "%@는 스팸으로 신고됐습니다."; -"Common.Alerts.SignOutUserConfirm.Message" = "Do you want to sign out?"; -"Common.Alerts.SignOutUserConfirm.Title" = "Sign out"; +"Common.Alerts.SignOutUserConfirm.Message" = "로그아웃 할까요?"; +"Common.Alerts.SignOutUserConfirm.Title" = "로그아웃 됐습니다."; "Common.Alerts.TooManyRequests.Title" = "요청이 몰렸습니다."; -"Common.Alerts.TootDeleted.Title" = "Toot Deleted"; -"Common.Alerts.TootFail.DraftSavedMessage" = "Your toot has been saved to Drafts."; +"Common.Alerts.TootDeleted.Title" = "툿을 지웠습니다."; +"Common.Alerts.TootFail.DraftSavedMessage" = "툿이 보관함으로 이동되었습니다."; "Common.Alerts.TootFail.Message" = "다시 해보세요."; -"Common.Alerts.TootFail.Title" = "Failed to Toot"; -"Common.Alerts.TootPosted.Title" = "Toot Posted"; -"Common.Alerts.TootSending.Title" = "Sending toot"; +"Common.Alerts.TootFail.Title" = "툿을 실패했습니다."; +"Common.Alerts.TootPosted.Title" = "툿했습니다."; +"Common.Alerts.TootSending.Title" = "툿을 보냅니다."; "Common.Alerts.TweetDeleted.Title" = "트윗을 지웠습니다."; -"Common.Alerts.TweetFail.DraftSavedMessage" = "Your tweet has been saved to Drafts."; +"Common.Alerts.TweetFail.DraftSavedMessage" = "트윗이 보관함으로 이동되었습니다."; "Common.Alerts.TweetFail.Message" = "다시 해보세요."; "Common.Alerts.TweetFail.Title" = "트윗이 올라가지 않았습니다."; -"Common.Alerts.TweetPosted.Title" = "Tweet Posted"; +"Common.Alerts.TweetPosted.Title" = "트윗했습니다."; "Common.Alerts.TweetSending.Title" = "트윗을 보냅니다"; "Common.Alerts.TweetSent.Title" = "트윗 보냄"; -"Common.Alerts.UnblockUserConfirm.Title" = "Do you want to unblock %@?"; +"Common.Alerts.UnblockUserConfirm.Title" = "%@의 차단을 풀까요?"; "Common.Alerts.UnblockUserSuccess.Title" = "%@의 차단을 풉니다."; "Common.Alerts.UnfollowUser.Message" = "%@를 그만 팔로우할까요?"; "Common.Alerts.UnfollowingSuccess.Title" = "팔로우를 끊었습니다."; -"Common.Alerts.UnmuteUserConfirm.Title" = "Do you want to unmute %@?"; +"Common.Alerts.UnmuteUserConfirm.Title" = "%@를 이제 뮤트하지 말까요?"; "Common.Alerts.UnmuteUserSuccess.Title" = "%@를 뮤트하지 않습니다."; "Common.Controls.Actions.Add" = "새로 만들기"; -"Common.Controls.Actions.Browse" = "Browse"; +"Common.Controls.Actions.Browse" = "탐색"; "Common.Controls.Actions.Cancel" = "돌아가기"; "Common.Controls.Actions.Confirm" = "확인"; -"Common.Controls.Actions.Delete" = "Delete"; +"Common.Controls.Actions.Delete" = "지우기"; "Common.Controls.Actions.Edit" = "고치기"; "Common.Controls.Actions.Ok" = "확인"; "Common.Controls.Actions.OpenInSafari" = "사파리에서 보기"; @@ -138,18 +155,18 @@ "Common.Controls.Actions.SavePhoto" = "사진 저장하기"; "Common.Controls.Actions.ShareMedia" = "미디어 공유하기"; "Common.Controls.Actions.SignIn" = "로그인"; -"Common.Controls.Actions.SignOut" = "Sign out"; +"Common.Controls.Actions.SignOut" = "로그아웃 됐습니다."; "Common.Controls.Actions.TakePhoto" = "사진 찍기"; "Common.Controls.Actions.Yes" = "네"; "Common.Controls.Friendship.Actions.Block" = "차단하기"; -"Common.Controls.Friendship.Actions.Blocked" = "Blocked"; +"Common.Controls.Friendship.Actions.Blocked" = "차단됨"; "Common.Controls.Friendship.Actions.Follow" = "팔로우하기"; "Common.Controls.Friendship.Actions.Following" = "팔로우 중"; "Common.Controls.Friendship.Actions.Mute" = "숨기기"; "Common.Controls.Friendship.Actions.Pending" = "대기중"; "Common.Controls.Friendship.Actions.Report" = "신고하기"; "Common.Controls.Friendship.Actions.ReportAndBlock" = "신고하고 차단하기"; -"Common.Controls.Friendship.Actions.Request" = "Request"; +"Common.Controls.Friendship.Actions.Request" = "요청"; "Common.Controls.Friendship.Actions.Unblock" = "차단 풀기"; "Common.Controls.Friendship.Actions.Unfollow" = "팔로우 끊기"; "Common.Controls.Friendship.Actions.Unmute" = "숨기기 풀기"; @@ -161,21 +178,23 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@가 날 팔로우하고 있음"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@가 날 팔로우하지 않음"; "Common.Controls.Ios.PhotoLibrary" = "갤러리"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "결과 없음"; "Common.Controls.ProfileDashboard.Followers" = "팔로워"; "Common.Controls.ProfileDashboard.Following" = "팔로우 중"; "Common.Controls.ProfileDashboard.Listed" = "담긴 리스트"; "Common.Controls.Status.Actions.Bookmark" = "즐겨찾기"; +"Common.Controls.Status.Actions.Boost" = "Boost"; "Common.Controls.Status.Actions.CopyLink" = "링크 복사하기"; "Common.Controls.Status.Actions.CopyText" = "글 복사하기"; "Common.Controls.Status.Actions.DeleteTweet" = "트윗 지우기"; -"Common.Controls.Status.Actions.PinOnProfile" = "Pin on Profile"; +"Common.Controls.Status.Actions.PinOnProfile" = "프로필에 고정"; "Common.Controls.Status.Actions.Quote" = "인용"; "Common.Controls.Status.Actions.Retweet" = "리트윗"; -"Common.Controls.Status.Actions.Share" = "Share"; +"Common.Controls.Status.Actions.Share" = "공유"; +"Common.Controls.Status.Actions.ShareContent" = "Share content"; "Common.Controls.Status.Actions.ShareLink" = "링크 공유하기"; -"Common.Controls.Status.Actions.Translate" = "Translate"; -"Common.Controls.Status.Actions.UnpinFromProfile" = "Unpin from Profile"; +"Common.Controls.Status.Actions.Translate" = "번역"; +"Common.Controls.Status.Actions.UnpinFromProfile" = "프로필에서 고정 풀기"; "Common.Controls.Status.Actions.Vote" = "투표"; "Common.Controls.Status.Media" = "미디어"; "Common.Controls.Status.Poll.Expired" = "닫힘"; @@ -184,10 +203,10 @@ "Common.Controls.Status.Poll.TotalVote" = "%@표"; "Common.Controls.Status.Poll.TotalVotes" = "%@표"; "Common.Controls.Status.Thread.Show" = "이 타래 보기"; -"Common.Controls.Status.UserBoosted" = "%@ boosted"; +"Common.Controls.Status.UserBoosted" = "%@이 부스트했습니다."; "Common.Controls.Status.UserRetweeted" = "%@가 리트윗함"; -"Common.Controls.Status.YouBoosted" = "You boosted"; -"Common.Controls.Status.YouRetweeted" = "You retweeted"; +"Common.Controls.Status.YouBoosted" = "내가 부스트했습니다."; +"Common.Controls.Status.YouRetweeted" = "내가 리트윗했습니다."; "Common.Controls.Timeline.LoadMore" = "더 불러오기"; "Common.Countable.Like.Multiple" = "%@개 좋아요"; "Common.Countable.Like.Single" = "%@개 좋아요"; @@ -238,10 +257,10 @@ "Scene.Compose.Visibility.Private" = "비공개"; "Scene.Compose.Visibility.Public" = "공개"; "Scene.Compose.Visibility.Unlisted" = "리스트 되지 않음"; -"Scene.Compose.VisibilityDescription.Direct" = "Visible for mentioned users only"; -"Scene.Compose.VisibilityDescription.Private" = "Visible for followers only"; -"Scene.Compose.VisibilityDescription.Public" = "Visible for all, shown in public timelines"; -"Scene.Compose.VisibilityDescription.Unlisted" = "Visible for all, but not in public timelines"; +"Scene.Compose.VisibilityDescription.Direct" = "멘션한 사람에게만 보이기"; +"Scene.Compose.VisibilityDescription.Private" = "팔로워들에게만 보이기"; +"Scene.Compose.VisibilityDescription.Public" = "모두에게 보이기, 퍼블릭 타임라인에 나타납니다."; +"Scene.Compose.VisibilityDescription.Unlisted" = "모두에게 보이지만, 퍼블릭 타임라인에 나타나지 않습니다."; "Scene.Compose.Vote.Expiration.1Day" = "하루"; "Scene.Compose.Vote.Expiration.1Hour" = "1시간"; "Scene.Compose.Vote.Expiration.30Min" = "30분"; @@ -250,7 +269,7 @@ "Scene.Compose.Vote.Expiration.6Hour" = "6시간"; "Scene.Compose.Vote.Expiration.7Day" = "일주일"; "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" = "지우기"; @@ -258,7 +277,7 @@ "Scene.Drafts.Title" = "임시 보관함"; "Scene.Drawer.ManageAccounts" = "계정 관리"; "Scene.Drawer.SignIn" = "로그인"; -"Scene.Federated.Title" = "Federated"; +"Scene.Federated.Title" = "연합"; "Scene.Followers.Title" = "팔로워"; "Scene.Following.Title" = "팔로우 중"; "Scene.Likes.Title" = "좋아요"; @@ -296,13 +315,13 @@ "Scene.ListsUsers.Add.Title" = "멤버 추가"; "Scene.ListsUsers.MenuActions.Add" = "새로 만들기"; "Scene.ListsUsers.MenuActions.Remove" = "지우기"; -"Scene.Local.Title" = "Local"; +"Scene.Local.Title" = "로컬"; "Scene.ManageAccounts.DeleteAccount" = "계정 지우기"; "Scene.ManageAccounts.Title" = "계정"; "Scene.Mentions.Title" = "답글"; "Scene.Messages.Action.CopyText" = "메시지 글자 복사"; "Scene.Messages.Action.Delete" = "메시지 지우기"; -"Scene.Messages.Error.NotSupported" = "The Current account does not support direct messages"; +"Scene.Messages.Error.NotSupported" = "이 계정은 DM을 보낼 수 없습니다."; "Scene.Messages.Expanded.Photo" = "[사진]"; "Scene.Messages.Icon.Failed" = "메시지를 보내지 못했습니다"; "Scene.Messages.NewConversation.Search" = "사람 찾기"; @@ -311,7 +330,7 @@ "Scene.Notification.Tabs.All" = "모두"; "Scene.Notification.Tabs.Mentions" = "답글"; "Scene.Notification.Title" = "알림"; -"Scene.Profile.Fields.JoinedInDate" = "Joined in %@"; +"Scene.Profile.Fields.JoinedInDate" = "%@에 가입함"; "Scene.Profile.Filter.All" = "모든 트윗"; "Scene.Profile.Filter.ExcludeReplies" = "답글 빼기"; "Scene.Profile.HideReply" = "답글 숨기기"; @@ -324,8 +343,8 @@ "Scene.Search.ShowMore" = "더 보기"; "Scene.Search.Tabs.Hashtag" = "해시태그"; "Scene.Search.Tabs.Media" = "미디어"; -"Scene.Search.Tabs.People" = "People"; -"Scene.Search.Tabs.Toots" = "Toots"; +"Scene.Search.Tabs.People" = "사람들"; +"Scene.Search.Tabs.Toots" = "툿"; "Scene.Search.Tabs.Tweets" = "트윗"; "Scene.Search.Tabs.Users" = "사용자"; "Scene.Search.Title" = "찾기"; @@ -357,7 +376,7 @@ "Scene.Settings.Display.Media.AutoPlayback" = "자동 재생"; "Scene.Settings.Display.Media.Automatic" = "자동"; "Scene.Settings.Display.Media.MediaPreviews" = "미디어 미리보기"; -"Scene.Settings.Display.Media.MuteByDefault" = "Mute by default"; +"Scene.Settings.Display.Media.MuteByDefault" = "기본으로 숨기기"; "Scene.Settings.Display.Media.Off" = "끄기"; "Scene.Settings.Display.Preview.ThankForUsingTwidereX" = "@TwidereProject를 써주셔서 고맙습니다!"; "Scene.Settings.Display.SectionHeader.DateFormat" = "날짜 형식"; @@ -381,7 +400,7 @@ "Scene.Settings.Misc.Nitter.Dialog.Usage.ProjectButton" = "프로젝트 소개 보기"; "Scene.Settings.Misc.Nitter.Dialog.Usage.Title" = "다음의 제3 트위터 정보 제공자를 사용 중"; "Scene.Settings.Misc.Nitter.Input.Description" = "개인정보보호에 초점을 둔 트위터 프론트엔드 대체 기능"; -"Scene.Settings.Misc.Nitter.Input.Invalid" = "Nitter instance URL is invalid, e.g. https://nitter.net"; +"Scene.Settings.Misc.Nitter.Input.Invalid" = "Nitter의 Instance URL이 유효하지 않습니다. 예) https://nitter.net"; "Scene.Settings.Misc.Nitter.Input.Placeholder" = "Nitter 인스턴스"; "Scene.Settings.Misc.Nitter.Input.Value" = "Instance URL"; "Scene.Settings.Misc.Nitter.Title" = "제3의 트위터 정보 제공자"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict index 5c725633..23182c92 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/ko.lproj/Localizable.stringsdict @@ -2,6 +2,132 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld media + + + count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld characters + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + posts + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld posts + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld quotes + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld likes + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld replies + + count.vote NSStringLocalizedFormatKey @@ -13,7 +139,7 @@ NSStringFormatValueTypeKey ld other - %ld votes + %ld 표 count.people @@ -27,7 +153,7 @@ NSStringFormatValueTypeKey ld other - %ld people + %ld 명 count.people.talking @@ -41,7 +167,7 @@ NSStringFormatValueTypeKey ld other - %ld people talking + %ld 명이 이야기 중 date.year.left @@ -55,7 +181,7 @@ NSStringFormatValueTypeKey ld other - %ld years left + %ld년 남음 date.month.left @@ -69,7 +195,7 @@ NSStringFormatValueTypeKey ld other - %ld months left + %ld달 남음 date.day.left @@ -83,7 +209,7 @@ NSStringFormatValueTypeKey ld other - %ld days left + %ld일 남음 date.hour.left @@ -97,7 +223,7 @@ NSStringFormatValueTypeKey ld other - %ld hours left + %ld시간 남음 date.minute.left @@ -111,7 +237,7 @@ NSStringFormatValueTypeKey ld other - %ld minutes left + %ld분 남음 date.second.left @@ -125,7 +251,7 @@ NSStringFormatValueTypeKey ld other - %ld seconds left + %ld초 남음 diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings index a2b566a3..c7491656 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Logo do Twitter"; "Accessibility.Common.More" = "Mais"; "Accessibility.Common.NetworkImage" = "Imagem da Rede"; +"Accessibility.Common.Status.Actions.HideContent" = "Hide content"; +"Accessibility.Common.Status.Actions.HideMedia" = "Hide media"; "Accessibility.Common.Status.Actions.Like" = "Curtir"; +"Accessibility.Common.Status.Actions.Menu" = "Menu"; "Accessibility.Common.Status.Actions.Reply" = "Responder"; "Accessibility.Common.Status.Actions.Retweet" = "Retweet"; +"Accessibility.Common.Status.Actions.RevealContent" = "Reveal content"; +"Accessibility.Common.Status.Actions.RevealMedia" = "Reveal media"; +"Accessibility.Common.Status.AuthorAvatar" = "Avatar do autor"; +"Accessibility.Common.Status.Boosted" = "Boosted"; +"Accessibility.Common.Status.ContentWarning" = "Content Warning"; +"Accessibility.Common.Status.Liked" = "Liked"; "Accessibility.Common.Status.Location" = "Localização"; "Accessibility.Common.Status.Media" = "Mídia"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "Poll option"; "Accessibility.Common.Status.Retweeted" = "Retweetado"; "Accessibility.Common.Video.Play" = "Reproduzir vídeo"; "Accessibility.Scene.Compose.AddMention" = "Adicionar menção"; @@ -32,15 +42,22 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "Abaixar Conta"; "Accessibility.Scene.Home.Menu" = "Menu"; "Accessibility.Scene.ManageAccounts.Add" = "Adicionar"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "Usuário de login atual: %@"; "Accessibility.Scene.Search.History" = "Histórico"; "Accessibility.Scene.Search.Save" = "Salvar"; "Accessibility.Scene.Settings.Display.FontSize" = "Tamanho da Fonte"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "Insira o domínio Mastodon para fazer login"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Configuração da chave de autenticação do cliente do Twitter"; "Accessibility.Scene.Timeline.LoadGap" = "Carregar"; "Accessibility.Scene.User.Location" = "Localização"; "Accessibility.Scene.User.Tab.Favourite" = "Favorito"; "Accessibility.Scene.User.Tab.Media" = "Mídia"; "Accessibility.Scene.User.Tab.Status" = "Status"; "Accessibility.Scene.User.Website" = "Site"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "Toque duas vezes e segure para exibir o menu"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "Toque duas vezes e segure para abrir o painel de contas"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "Toque duas vezes para abrir o perfil"; +"Accessibility.VoiceOver.Selected" = "Selected"; "Common.Alerts.AccountSuspended.Message" = "O Twitter suspende contas que violam as %@"; "Common.Alerts.AccountSuspended.Title" = "Conta Suspensa"; "Common.Alerts.AccountSuspended.TwitterRules" = "Regras do Twitter"; @@ -90,7 +107,7 @@ "Common.Alerts.MediaSharing.Title" = "A mídia será compartilhada depois que o download for concluído"; "Common.Alerts.MuteUserConfirm.Title" = "Você quer silenciar %@?"; "Common.Alerts.MuteUserSuccess.Title" = "%@ foi silenciado"; -"Common.Alerts.NoTweetsFound.Title" = "Nenhum Tweet Encontrado"; +"Common.Alerts.NoTweetsFound.Title" = "Sem Tweets Encontrados"; "Common.Alerts.PermissionDeniedFriendshipBlocked.Message" = "Você foi impedido de seguir esta conta a pedido do usuário"; "Common.Alerts.PermissionDeniedFriendshipBlocked.Title" = "Permissão Negada"; "Common.Alerts.PermissionDeniedNotAuthorized.Message" = "Desculpe, você não está autorizado"; @@ -104,7 +121,7 @@ "Common.Alerts.ReportUserSuccess.Title" = "%@ foi reportado por spam"; "Common.Alerts.SignOutUserConfirm.Message" = "Você quer encerrar esta sessão?"; "Common.Alerts.SignOutUserConfirm.Title" = "Encerrar sessão"; -"Common.Alerts.TooManyRequests.Title" = "Muitas solicitações"; +"Common.Alerts.TooManyRequests.Title" = "Muitos Pedidos"; "Common.Alerts.TootDeleted.Title" = "Toot Excluído"; "Common.Alerts.TootFail.DraftSavedMessage" = "Seu toot foi salvo nos Rascunhos."; "Common.Alerts.TootFail.Message" = "Por favor, tente novamente"; @@ -161,11 +178,12 @@ "Common.Controls.Friendship.UserIsFollowingYou" = "%@ está seguindo você"; "Common.Controls.Friendship.UserIsNotFollowingYou" = "%@ não está seguindo você"; "Common.Controls.Ios.PhotoLibrary" = "Biblioteca de Fotos"; -"Common.Controls.List.NoResults" = "No results"; +"Common.Controls.List.NoResults" = "Nenhum resultado"; "Common.Controls.ProfileDashboard.Followers" = "Seguidores"; "Common.Controls.ProfileDashboard.Following" = "Seguindo"; "Common.Controls.ProfileDashboard.Listed" = "Listado"; "Common.Controls.Status.Actions.Bookmark" = "Favorito"; +"Common.Controls.Status.Actions.Boost" = "Impulso"; "Common.Controls.Status.Actions.CopyLink" = "Copiar link"; "Common.Controls.Status.Actions.CopyText" = "Copiar texto"; "Common.Controls.Status.Actions.DeleteTweet" = "Excluir tweet"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "Citação"; "Common.Controls.Status.Actions.Retweet" = "Retweet"; "Common.Controls.Status.Actions.Share" = "Compartilhar"; +"Common.Controls.Status.Actions.ShareContent" = "Compartilhar conteúdo"; "Common.Controls.Status.Actions.ShareLink" = "Compartilhar link"; "Common.Controls.Status.Actions.Translate" = "Traduzir"; "Common.Controls.Status.Actions.UnpinFromProfile" = "Desafixar do Perfil"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict index cc5e803c..f1b55c10 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/pt-BR.lproj/Localizable.stringsdict @@ -2,6 +2,150 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 mídia + other + %ld mídias + + + count.input_limit_remains + + NSStringLocalizedFormatKey + O limite de entrada permanece %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 caractere + other + %ld caracteres + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + postagem + other + postagens + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 postagem + other + %ld postagens + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 citação + other + %ld citações + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 curtida + other + %ld curtidas + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 retweet + other + %ld retweets + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 resposta + other + %ld respostas + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings index 79f1be48..234b4a5a 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.strings @@ -8,11 +8,21 @@ "Accessibility.Common.Logo.Twitter" = "Twitter 徽标"; "Accessibility.Common.More" = "更多"; "Accessibility.Common.NetworkImage" = "网络图像"; +"Accessibility.Common.Status.Actions.HideContent" = "隐藏内容"; +"Accessibility.Common.Status.Actions.HideMedia" = "隐藏媒体"; "Accessibility.Common.Status.Actions.Like" = "喜欢"; +"Accessibility.Common.Status.Actions.Menu" = "菜单"; "Accessibility.Common.Status.Actions.Reply" = "回复"; "Accessibility.Common.Status.Actions.Retweet" = "转推"; +"Accessibility.Common.Status.Actions.RevealContent" = "显示内容"; +"Accessibility.Common.Status.Actions.RevealMedia" = "显示媒体"; +"Accessibility.Common.Status.AuthorAvatar" = "作者头像"; +"Accessibility.Common.Status.Boosted" = "已转嘟"; +"Accessibility.Common.Status.ContentWarning" = "内容警告"; +"Accessibility.Common.Status.Liked" = "已喜欢"; "Accessibility.Common.Status.Location" = "地理位置"; "Accessibility.Common.Status.Media" = "媒体"; +"Accessibility.Common.Status.PollOptionOrdinalPrefix" = "投票选项"; "Accessibility.Common.Status.Retweeted" = "转推了"; "Accessibility.Common.Video.Play" = "播放视频"; "Accessibility.Scene.Compose.AddMention" = "添加提及"; @@ -32,20 +42,27 @@ "Accessibility.Scene.Home.Drawer.AccountDropdown" = "账户下拉列表"; "Accessibility.Scene.Home.Menu" = "菜单"; "Accessibility.Scene.ManageAccounts.Add" = "添加"; +"Accessibility.Scene.ManageAccounts.CurrentSignInUser" = "当前登录用户:%@"; "Accessibility.Scene.Search.History" = "历史"; "Accessibility.Scene.Search.Save" = "保存"; "Accessibility.Scene.Settings.Display.FontSize" = "字体大小"; +"Accessibility.Scene.SignIn.PleaseEnterMastodonDomainToSignIn" = "请输入要登录的 Mastodon 域名"; +"Accessibility.Scene.SignIn.TwitterClientAuthenticationKeySetting" = "Twitter 客户端认证密钥自定义设置"; "Accessibility.Scene.Timeline.LoadGap" = "加载"; "Accessibility.Scene.User.Location" = "地理位置"; "Accessibility.Scene.User.Tab.Favourite" = "收藏"; "Accessibility.Scene.User.Tab.Media" = "媒体"; "Accessibility.Scene.User.Tab.Status" = "推文"; "Accessibility.Scene.User.Website" = "站点"; +"Accessibility.VoiceOver.DoubleTapAndHoldToDisplayMenu" = "轻点两下并按住以显示菜单"; +"Accessibility.VoiceOver.DoubleTapAndHoldToOpenTheAccountsPanel" = "轻点两下并按住打开账户列表"; +"Accessibility.VoiceOver.DoubleTapToOpenProfile" = "轻点两下打开个人资料"; +"Accessibility.VoiceOver.Selected" = "已选中"; "Common.Alerts.AccountSuspended.Message" = "Twitter 会冻结违反 %@ 的账号"; "Common.Alerts.AccountSuspended.Title" = "账号已被冻结"; "Common.Alerts.AccountSuspended.TwitterRules" = "Twitter 规则"; "Common.Alerts.AccountTemporarilyLocked.Message" = "打开 Twitter 以解锁"; -"Common.Alerts.AccountTemporarilyLocked.Title" = "帐户临时锁定"; +"Common.Alerts.AccountTemporarilyLocked.Title" = "账户临时锁定"; "Common.Alerts.BlockUserConfirm.Title" = "是否要屏蔽 %@?"; "Common.Alerts.BlockUserSuccess.Title" = "%@ 已被屏蔽"; "Common.Alerts.CancelFollowRequest.Message" = "取消关注 %@ 的请求?"; @@ -166,6 +183,7 @@ "Common.Controls.ProfileDashboard.Following" = "已关注"; "Common.Controls.ProfileDashboard.Listed" = "列表中"; "Common.Controls.Status.Actions.Bookmark" = "书签"; +"Common.Controls.Status.Actions.Boost" = "转嘟"; "Common.Controls.Status.Actions.CopyLink" = "复制链接"; "Common.Controls.Status.Actions.CopyText" = "复制文本"; "Common.Controls.Status.Actions.DeleteTweet" = "删除推文"; @@ -173,6 +191,7 @@ "Common.Controls.Status.Actions.Quote" = "引用"; "Common.Controls.Status.Actions.Retweet" = "转推"; "Common.Controls.Status.Actions.Share" = "分享"; +"Common.Controls.Status.Actions.ShareContent" = "分享内容"; "Common.Controls.Status.Actions.ShareLink" = "分享链接"; "Common.Controls.Status.Actions.Translate" = "翻译"; "Common.Controls.Status.Actions.UnpinFromProfile" = "在个人资料页面取消置顶"; diff --git a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict index 0637774a..3bf18685 100644 --- a/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict +++ b/TwidereSDK/Sources/TwidereLocalization/Resources/zh-Hans.lproj/Localizable.stringsdict @@ -2,6 +2,132 @@ + count.media + + NSStringLocalizedFormatKey + %#@count_media@ + count_media + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个媒体 + + + count.input_limit_remains + + NSStringLocalizedFormatKey + 输入字符限制剩余 %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个字符 + + + count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + 帖子 + + + count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个帖子 + + + count.quote + + NSStringLocalizedFormatKey + %#@quote_count@ + quote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个引用 + + + count.like + + NSStringLocalizedFormatKey + %#@like_count@ + like_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个赞 + + + count.retweet + + NSStringLocalizedFormatKey + %#@retweet_count@ + retweet_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 条转推 + + + count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 条转嘟 + + + count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + %ld 个回复 + + count.vote NSStringLocalizedFormatKey diff --git a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift index 65829b06..16cf1e1a 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/MediaGridContainerView.swift @@ -59,6 +59,9 @@ public final class MediaGridContainerView: UIView { let sensitiveToggleButton: HitTestExpandedButton = { let button = HitTestExpandedButton(type: .system) button.setImage(Asset.Human.eyeSlashMini.image.withRenderingMode(.alwaysTemplate), for: .normal) + button.isAccessibilityElement = true + button.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.hideMedia + button.accessibilityTraits = .button return button }() @@ -67,6 +70,9 @@ public final class MediaGridContainerView: UIView { overlay.layer.masksToBounds = true overlay.layer.cornerRadius = MediaView.cornerRadius overlay.layer.cornerCurve = .continuous + overlay.isAccessibilityElement = true + overlay.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.revealMedia + overlay.accessibilityTraits = .button return overlay }() @@ -335,3 +341,17 @@ extension MediaGridContainerView: ContentWarningOverlayViewDelegate { delegate?.mediaGridContainerView(self, toggleContentWarningOverlayViewDisplay: contentWarningOverlayView) } } + +extension MediaGridContainerView { + public override var accessibilityElements: [Any]? { + get { + if viewModel.isContentWarningOverlayDisplay == true { + return [contentWarningOverlayView] + } else { + return [sensitiveToggleButton] + mediaViews + } + + } + set { } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift b/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift index d6922e93..ee73499e 100644 --- a/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift +++ b/TwidereSDK/Sources/TwidereUI/Container/ProfileAvatarView.swift @@ -11,16 +11,12 @@ import Combine public final class ProfileAvatarView: UIView { - static let primitiveAvatarButtonSize = CGSize(width: 88, height: 88) - static let primitiveBadgeImageViewSize = CGSize(width: 24, height: 24) - var disposeBag = Set() public let avatarContainerView = UIView() public let avatarButton: AvatarButton = { - let frame = CGRect(origin: .zero, size: ProfileAvatarView.primitiveAvatarButtonSize) - let button = AvatarButton(frame: frame) + let button = AvatarButton(frame: .zero) button.avatarImageView.image = .placeholder(color: .systemFill) return button }() @@ -32,23 +28,19 @@ public final class ProfileAvatarView: UIView { var badgeImageViewHeightLayoutConstraint: NSLayoutConstraint! private let badgeMaskImageView = UIImageView() + + private(set) var dimension: Dimension? - let layoutDidChange = PassthroughSubject() - - public var dimension: CGFloat = 44.0 { - didSet { - updateScale() - } - } @Published public var avatarStyle: CornerStyle = .circle @Published public var badge: Badge = .none + let layoutDidChange = PassthroughSubject() - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } @@ -56,6 +48,7 @@ public final class ProfileAvatarView: UIView { } extension ProfileAvatarView { + private func _init() { backgroundColor = .clear avatarContainerView.backgroundColor = .systemBackground @@ -63,36 +56,10 @@ extension ProfileAvatarView { avatarContainerView.translatesAutoresizingMaskIntoConstraints = false addSubview(avatarContainerView) NSLayoutConstraint.activate([ - avatarContainerView.topAnchor.constraint(equalTo: topAnchor), - avatarContainerView.leadingAnchor.constraint(equalTo: leadingAnchor), - avatarContainerView.trailingAnchor.constraint(equalTo: trailingAnchor), - avatarContainerView.bottomAnchor.constraint(equalTo: bottomAnchor), + avatarContainerView.centerXAnchor.constraint(equalTo: centerXAnchor), + avatarContainerView.centerYAnchor.constraint(equalTo: centerYAnchor), ]) - avatarButton.translatesAutoresizingMaskIntoConstraints = false - avatarContainerView.addSubview(avatarButton) - avatarButtonWidthLayoutConstraint = avatarButton.widthAnchor.constraint(equalToConstant: ProfileAvatarView.primitiveAvatarButtonSize.width).priority(.required - 1) - avatarButtonHeightLayoutConstraint = avatarButton.heightAnchor.constraint(equalToConstant: ProfileAvatarView.primitiveAvatarButtonSize.height).priority(.required - 1) - NSLayoutConstraint.activate([ - avatarButton.topAnchor.constraint(equalTo: avatarContainerView.topAnchor), - avatarButton.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor), - avatarContainerView.trailingAnchor.constraint(equalTo: avatarButton.trailingAnchor), - avatarContainerView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor), - avatarButtonWidthLayoutConstraint, - avatarButtonHeightLayoutConstraint, - ]) - - badgeImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(badgeImageView) - badgeImageViewWidthLayoutConstraint = badgeImageView.widthAnchor.constraint(equalToConstant: ProfileAvatarView.primitiveBadgeImageViewSize.width).priority(.required - 1) - badgeImageViewHeightLayoutConstraint = badgeImageView.heightAnchor.constraint(equalToConstant: ProfileAvatarView.primitiveBadgeImageViewSize.height).priority(.required - 1) - NSLayoutConstraint.activate([ - badgeImageView.trailingAnchor.constraint(equalTo: avatarButton.trailingAnchor), - badgeImageView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor), - badgeImageViewWidthLayoutConstraint, - badgeImageViewHeightLayoutConstraint, - ]) - Publishers.CombineLatest3( $avatarStyle, $badge, @@ -104,40 +71,156 @@ extension ProfileAvatarView { } .store(in: &disposeBag) - updateScale() + setNeedsUpdateConstraints() } public override func layoutSubviews() { super.layoutSubviews() + setNeedsUpdateConstraints() layoutDidChange.send() } + public override func updateConstraints() { + super.updateConstraints() + + // assert width equalt to height + + guard let dimension = self.dimension else { return } + self.avatarButtonWidthLayoutConstraint.constant = bounds.width + self.avatarButtonHeightLayoutConstraint.constant = bounds.height + + let scale = bounds.width / dimension.primitiveAvatarButtonSize.width + let badgeDimension = dimension.primitiveBadgeImageViewSize.width * scale + self.badgeImageViewWidthLayoutConstraint.constant = badgeDimension + self.badgeImageViewHeightLayoutConstraint.constant = badgeDimension + + self.setNeedsLayout() + } + + public func setup(dimension: Dimension) { + guard self.dimension == nil else { + assertionFailure("only setup once") + return + } + + self.dimension = dimension + dimension.layout(view: self) + setNeedsLayout() + } + } extension ProfileAvatarView { + + public enum Dimension { + case inline + case plain + + var primitiveAvatarButtonSize: CGSize { + switch self { + case .inline: return CGSize(width: 44, height: 44) + case .plain: return CGSize(width: 88, height: 88) + } + } + + var primitiveBadgeImageViewSize: CGSize { + return CGSize(width: 24, height: 24) + } + + func layout(view: ProfileAvatarView) { + switch self { + case .inline: layoutInline(view: view) + case .plain: layoutPlain(view: view) + } + } + } + +} - func updateScale() { - self.avatarButtonWidthLayoutConstraint.constant = dimension - self.avatarButtonHeightLayoutConstraint.constant = dimension +extension ProfileAvatarView.Dimension { + func layoutInline(view: ProfileAvatarView) { + guard let dimension = view.dimension else { + assertionFailure() + return + } - let scale = dimension / ProfileAvatarView.primitiveAvatarButtonSize.width - let badgeDimension = ProfileAvatarView.primitiveBadgeImageViewSize.width * scale - self.badgeImageViewWidthLayoutConstraint.constant = badgeDimension - self.badgeImageViewHeightLayoutConstraint.constant = badgeDimension + view.avatarButton.translatesAutoresizingMaskIntoConstraints = false + view.avatarContainerView.addSubview(view.avatarButton) + view.avatarButtonWidthLayoutConstraint = view.avatarButton.widthAnchor.constraint(equalToConstant: dimension.primitiveAvatarButtonSize.width).priority(.required - 1) + view.avatarButtonHeightLayoutConstraint = view.avatarButton.heightAnchor.constraint(equalToConstant: dimension.primitiveAvatarButtonSize.height).priority(.required - 1) + NSLayoutConstraint.activate([ + view.avatarButton.topAnchor.constraint(equalTo: view.avatarContainerView.topAnchor), + view.avatarButton.leadingAnchor.constraint(equalTo: view.avatarContainerView.leadingAnchor), + view.avatarContainerView.trailingAnchor.constraint(equalTo: view.avatarButton.trailingAnchor), + view.avatarContainerView.bottomAnchor.constraint(equalTo: view.avatarButton.bottomAnchor), + view.avatarButtonWidthLayoutConstraint, + view.avatarButtonHeightLayoutConstraint, + ]) - self.setNeedsLayout() + view.badgeImageView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(view.badgeImageView) + view.badgeImageViewWidthLayoutConstraint = view.badgeImageView.widthAnchor.constraint(equalToConstant: dimension.primitiveBadgeImageViewSize.width).priority(.required - 1) + view.badgeImageViewHeightLayoutConstraint = view.badgeImageView.heightAnchor.constraint(equalToConstant: dimension.primitiveBadgeImageViewSize.height).priority(.required - 1) + NSLayoutConstraint.activate([ + view.badgeImageView.trailingAnchor.constraint(equalTo: view.avatarButton.trailingAnchor, constant: 4), // offset 4pt + view.badgeImageView.bottomAnchor.constraint(equalTo: view.avatarButton.bottomAnchor, constant: 2), // offset 2pt + view.badgeImageViewWidthLayoutConstraint, + view.badgeImageViewHeightLayoutConstraint, + ]) } + func layoutPlain(view: ProfileAvatarView) { + guard let dimension = view.dimension else { + assertionFailure() + return + } + + view.avatarButton.translatesAutoresizingMaskIntoConstraints = false + view.avatarContainerView.addSubview(view.avatarButton) + view.avatarButtonWidthLayoutConstraint = view.avatarButton.widthAnchor.constraint(equalToConstant: dimension.primitiveAvatarButtonSize.width).priority(.required - 1) + view.avatarButtonHeightLayoutConstraint = view.avatarButton.heightAnchor.constraint(equalToConstant: dimension.primitiveAvatarButtonSize.height).priority(.required - 1) + NSLayoutConstraint.activate([ + view.avatarButton.topAnchor.constraint(equalTo: view.avatarContainerView.topAnchor), + view.avatarButton.leadingAnchor.constraint(equalTo: view.avatarContainerView.leadingAnchor), + view.avatarContainerView.trailingAnchor.constraint(equalTo: view.avatarButton.trailingAnchor), + view.avatarContainerView.bottomAnchor.constraint(equalTo: view.avatarButton.bottomAnchor), + view.avatarButtonWidthLayoutConstraint, + view.avatarButtonHeightLayoutConstraint, + ]) + + view.badgeImageView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(view.badgeImageView) + view.badgeImageViewWidthLayoutConstraint = view.badgeImageView.widthAnchor.constraint(equalToConstant: dimension.primitiveBadgeImageViewSize.width).priority(.required - 1) + view.badgeImageViewHeightLayoutConstraint = view.badgeImageView.heightAnchor.constraint(equalToConstant: dimension.primitiveBadgeImageViewSize.height).priority(.required - 1) + NSLayoutConstraint.activate([ + view.badgeImageView.trailingAnchor.constraint(equalTo: view.avatarButton.trailingAnchor), + view.badgeImageView.bottomAnchor.constraint(equalTo: view.avatarButton.bottomAnchor), + view.badgeImageViewWidthLayoutConstraint, + view.badgeImageViewHeightLayoutConstraint, + ]) + } } extension ProfileAvatarView { public enum Badge { case none - case circle + case circle(CircleBadge) case robot case verified + + public enum CircleBadge { + case twitter + case mastodon + + var image: UIImage { + switch self { + case .twitter: return Asset.Badge.circleTwitter.image + case .mastodon: return Asset.Badge.circleMastodon.image + } + } + } } public enum CornerStyle { @@ -146,19 +229,28 @@ extension ProfileAvatarView { } private func updateBadge() { + guard let dimension = self.dimension else { return } + // mask avatarButton let _avatarMaskImage: UIImage? = { - switch badge { - case .none: + switch (dimension, badge) { + case (_, .none): return nil - case .circle: - return Asset.Badge.circleMask.image - case .robot: - return Asset.Badge.robotMask.image - case .verified: - return Asset.Badge.verifiedMask.image + case (.inline, .circle): + return Asset.Badge.circleMask44.image + case (.plain, .circle): + return Asset.Badge.circleMask88.image + case (.inline, .robot): + return Asset.Badge.robotMask44.image + case (.plain, .robot): + return Asset.Badge.robotMask88.image + case (.inline, .verified): + return Asset.Badge.verifiedMask44.image + case (.plain, .verified): + return Asset.Badge.verifiedMask88.image } }() + if let avatarMaskImage = _avatarMaskImage { let frame = avatarButton.layer.frame let imageView = UIImageView(frame: frame) @@ -173,8 +265,8 @@ extension ProfileAvatarView { switch badge { case .none: badgeImageView.image = nil - case .circle: - break + case .circle(let circleBadge): + badgeImageView.image = circleBadge.image case .verified: badgeImageView.image = Asset.Badge.verified.image case .robot: diff --git a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift index 4a9b6a34..4556dd5c 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/MediaView.swift @@ -89,7 +89,7 @@ extension MediaView { private func _init() { // lazy load content later - isAccessibilityElement = true + imageView.isAccessibilityElement = true } public func setup(configuration: Configuration) { diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift index cdd33d61..cfc1a8b3 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView+ViewModel.swift @@ -7,6 +7,7 @@ import UIKit import Combine +import CoreData import CoreDataStack import TwidereAsset import TwitterMeta @@ -27,6 +28,9 @@ extension PollOptionView { public final class ViewModel: ObservableObject { var disposeBag = Set() var observations = Set() + var objects = Set() + + @Published var authenticationContext: AuthenticationContext? @Published var style: PollOptionView.Style? @@ -47,6 +51,8 @@ extension PollOptionView { @Published public var selectImageTintColor: UIColor = Asset.Colors.hightLight.color @Published public var isReveal: Bool = false + @Published public var groupedAccessibilityLabel = "" + init() { // corner $isMultiple @@ -98,6 +104,28 @@ extension PollOptionView { return isExpire || isPollVoted || isMyPoll } .assign(to: &$isReveal) + // groupedAccessibilityLabel + + Publishers.CombineLatest3( + $metaContent, + $percentage, + $isReveal + ) + .map { metaContent, percentage, isReveal -> String in + var strings: [String?] = [] + + metaContent.flatMap { strings.append($0.string) } + + if isReveal, + let percentage = percentage, + let string = PollOptionView.percentageFormatter.string(from: NSNumber(value: percentage)) + { + strings.append(string) + } + + return strings.compactMap { $0 }.joined(separator: ", ") + } + .assign(to: &$groupedAccessibilityLabel) } public enum Corner: Hashable { @@ -208,22 +236,26 @@ extension PollOptionView.ViewModel { $selectImageTintColor .assign(to: \.tintColor, on: view.selectionImageView) .store(in: &disposeBag) + // accessibility + $isSelect + .sink { isSelect in + if isSelect == true { + view.accessibilityTraits.insert(.selected) + } else { + view.accessibilityTraits.remove(.selected) + } + } + .store(in: &disposeBag) + $groupedAccessibilityLabel + .sink { groupedAccessibilityLabel in + view.accessibilityLabel = groupedAccessibilityLabel + } + .store(in: &disposeBag) } } extension PollOptionView { - public struct ConfigurationContext { - public let dateTimeProvider: DateTimeProvider - public let activeAuthenticationContext: AnyPublisher - - public init( - dateTimeProvider: DateTimeProvider, - activeAuthenticationContext: AnyPublisher - ) { - self.dateTimeProvider = dateTimeProvider - self.activeAuthenticationContext = activeAuthenticationContext - } - } + public typealias ConfigurationContext = StatusView.ConfigurationContext } extension PollOptionView { @@ -231,6 +263,12 @@ extension PollOptionView { pollOption option: MastodonPollOption, configurationContext: ConfigurationContext ) { + viewModel.objects.insert(option) + + configurationContext.authenticationContext + .assign(to: \.authenticationContext, on: viewModel) + .store(in: &disposeBag) + // metaContent Publishers.CombineLatest( option.poll.status.publisher(for: \.emojis), @@ -264,14 +302,14 @@ extension PollOptionView { option.publisher(for: \.poll), option.publisher(for: \.voteBy), option.publisher(for: \.isSelected), - configurationContext.activeAuthenticationContext + viewModel.$authenticationContext ) - .sink { [weak self] poll, optionVoteBy, isSelected, activeAuthenticationContext in + .sink { [weak self] poll, optionVoteBy, isSelected, authenticationContext in guard let self = self else { return } let domain: String let userID: String - switch activeAuthenticationContext { + switch authenticationContext { case .twitter, .none: domain = "" userID = "" diff --git a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift index c6004ac4..9126c33b 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/PollOptionView.swift @@ -54,6 +54,7 @@ public final class PollOptionView: UIView { }() public func prepareForReuse() { + viewModel.objects.removeAll() viewModel.percentage = nil stripProgressView.setProgress(0, animated: false) } @@ -74,6 +75,10 @@ extension PollOptionView { private func _init() { textField.deleteBackwardDelegate = self + + // Accessibility + // hint: Poll option + accessibilityHint = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix } public override func layoutSubviews() { diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView+ViewModel.swift new file mode 100644 index 00000000..09320ed5 --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView+ViewModel.swift @@ -0,0 +1,85 @@ +// +// StatusMetricsDashboardView+ViewModel.swift +// +// +// Created by MainasuK on 2022-2-23. +// + +import UIKit +import Combine +import CoreDataStack + +extension StatusMetricsDashboardView { + public final class ViewModel: ObservableObject { + var disposeBag = Set() + + @Published public var platform: Platform = .none + + @Published public var replyCount: Int = 0 + @Published public var repostCount: Int = 0 + @Published public var quoteCount: Int = 0 + @Published public var likeCount: Int = 0 + + func bind(view: StatusMetricsDashboardView) { + // reply + $replyCount + .sink { count in + let text = ViewModel.metricText(count: count) + view.replyButton.setTitle(text, for: .normal) + view.replyButton.accessibilityLabel = L10n.Count.reply(count) + } + .store(in: &disposeBag) + // repost + Publishers.CombineLatest( + $repostCount, + $platform + ) + .sink { count, platform in + let text = ViewModel.metricText(count: count) + view.repostButton.setTitle(text, for: .normal) + + switch platform { + case .none: + view.repostButton.accessibilityLabel = nil + case .twitter: + view.repostButton.accessibilityLabel = L10n.Count.retweet(count) + case .mastodon: + view.repostButton.accessibilityLabel = L10n.Count.reblog(count) + } + } + .store(in: &disposeBag) + // quote + Publishers.CombineLatest( + $quoteCount, + $platform + ) + .sink { count, platform in + let text = ViewModel.metricText(count: count) + view.quoteButton.setTitle(text, for: .normal) + view.quoteButton.accessibilityLabel = L10n.Count.quote(count) + + view.quoteButton.isHidden = { + switch platform { + case .none: return true + case .twitter: return false + case .mastodon: return true + } + }() + } + .store(in: &disposeBag) + // like + $likeCount + .sink { count in + let text = ViewModel.metricText(count: count) + view.likeButton.setTitle(text, for: .normal) + view.likeButton.accessibilityLabel = L10n.Count.like(count) + } + .store(in: &disposeBag) + } + + private static func metricText(count: Int) -> String { + guard count >= 0 else { return "0" } + return StatusMetricsDashboardView.numberMetricFormatter.string(from: count) ?? "0" + } + } +} diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift index 2a9a6510..80825a70 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusMetricsDashboardView.swift @@ -7,6 +7,7 @@ import os.log import UIKit +import CoreDataStack import TwidereCommon import TwidereCore @@ -22,6 +23,12 @@ public final class StatusMetricsDashboardView: UIView { public weak var delegate: StatusMetricsDashboardViewDelegate? + public private(set) lazy var viewModel: ViewModel = { + let viewModel = ViewModel() + viewModel.bind(view: self) + return viewModel + }() + public let container = UIStackView() public let metaContainer = UIStackView() @@ -36,6 +43,7 @@ public final class StatusMetricsDashboardView: UIView { label.textAlignment = .center return label }() + public let dashboardContainer = UIStackView() public let replyButton = HitTestExpandedButton() @@ -76,8 +84,8 @@ extension StatusMetricsDashboardView { metaContainer.addArrangedSubview(sourceLabel) dashboardContainer.axis = .horizontal - dashboardContainer.spacing = 8 - dashboardContainer.distribution = .fillEqually + dashboardContainer.spacing = 20 + dashboardContainer.distribution = .equalCentering container.addArrangedSubview(dashboardContainer) let buttons = [replyButton, repostButton, quoteButton, likeButton] @@ -88,15 +96,16 @@ extension StatusMetricsDashboardView { button.setTitleColor(.secondaryLabel, for: .normal) button.setInsets(forContentPadding: .zero, imageTitlePadding: 4) button.addTarget(self, action: #selector(StatusMetricsDashboardView.buttonDidPressed(_:)), for: .touchUpInside) + + // TODO: coordinate to the user list + button.accessibilityTraits = .staticText } replyButton.setImage(Asset.Arrows.arrowTurnUpLeftMini.image.withRenderingMode(.alwaysTemplate), for: .normal) repostButton.setImage(Asset.Media.repeatMini.image.withRenderingMode(.alwaysTemplate), for: .normal) quoteButton.setImage(Asset.TextFormatting.textQuoteMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - likeButton.setImage(Asset.Health.heartMini.image.withRenderingMode(.alwaysTemplate), for: .normal) + likeButton.setImage(Asset.Health.heartFillMini.image.withRenderingMode(.alwaysTemplate), for: .normal) - let leadingPadding = UIView() - dashboardContainer.addArrangedSubview(leadingPadding) replyButton.translatesAutoresizingMaskIntoConstraints = false repostButton.translatesAutoresizingMaskIntoConstraints = false quoteButton.translatesAutoresizingMaskIntoConstraints = false @@ -105,9 +114,7 @@ extension StatusMetricsDashboardView { dashboardContainer.addArrangedSubview(repostButton) dashboardContainer.addArrangedSubview(quoteButton) dashboardContainer.addArrangedSubview(likeButton) - let trailingPadding = UIView() - dashboardContainer.addArrangedSubview(trailingPadding) - + NSLayoutConstraint.activate([ replyButton.heightAnchor.constraint(equalToConstant: 44).priority(.required - 10), replyButton.heightAnchor.constraint(equalTo: repostButton.heightAnchor).priority(.defaultHigh), @@ -150,38 +157,7 @@ extension StatusMetricsDashboardView { } extension StatusMetricsDashboardView { - - private func metricText(count: Int) -> String { - guard count > 0 else { return "" } - return StatusMetricsDashboardView.numberMetricFormatter.string(from: count) ?? "" - } - public func setupReply(count: Int) { - let text = metricText(count: count) - replyButton.setTitle(text, for: .normal) - replyButton.isHidden = count == 0 - - // FIXME: a11y - } - - public func setupRepost(count: Int) { - let text = metricText(count: count) - repostButton.setTitle(text, for: .normal) - repostButton.isHidden = count == 0 - } - - public func setupQuote(count: Int) { - let text = metricText(count: count) - quoteButton.setTitle(text, for: .normal) - quoteButton.isHidden = count == 0 - } - - public func setupLike(count: Int) { - let text = metricText(count: count) - likeButton.setTitle(text, for: .normal) - likeButton.isHidden = count == 0 - } - public func setDashboardDisplay() { dashboardContainer.isHidden = false } diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift index 097e12f0..2441f295 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView+ViewModel.swift @@ -20,6 +20,12 @@ import Meta extension StatusView { public final class ViewModel: ObservableObject { + static let pollOptionOrdinalNumberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .ordinal + return formatter + }() + var disposeBag = Set() var observations = Set() var objects = Set() @@ -27,27 +33,40 @@ extension StatusView { let logger = Logger(subsystem: "StatusView", category: "ViewModel") @Published public var platform: Platform = .none + @Published public var authenticationContext: AuthenticationContext? // me + @Published public var managedObjectContext: NSManagedObjectContext? @Published public var header: Header = .none + @Published public var userIdentifier: UserIdentifier? @Published public var authorAvatarImage: UIImage? @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? @Published public var authorUsername: String? @Published public var protected: Bool = false - + + @Published public var isMyself = false + @Published public var spoilerContent: MetaContent? + @Published public var content: MetaContent? + @Published public var twitterTextProvider: TwitterTextProvider? + @Published public var mediaViewConfigurations: [MediaView.Configuration] = [] - @Published public var isContentReveal: Bool = true + @Published public var isContentSensitive: Bool = false + @Published public var isContentSensitiveToggled: Bool = false + + @Published public var isContentReveal: Bool = false + @Published public var isMediaSensitive: Bool = false @Published public var isMediaSensitiveToggled: Bool = false - @Published public var isMediaReveal: Bool = false - + @Published public var isMediaSensitiveSwitchable = false + @Published public var isMediaReveal: Bool = false + // poll input @Published public var pollItems: [PollItem] = [] @Published public var isVotable: Bool = false @Published public var isVoting: Bool = false @@ -57,11 +76,17 @@ extension StatusView { @Published public var expireAt: Date? @Published public var expired: Bool = false + // poll output + @Published public var pollVoteDescription = "" + @Published public var pollCountdownDescription: String? + @Published public var location: String? @Published public var source: String? - @Published public var isRepost: Bool = false - @Published public var isLike: Bool = false + @Published public var isRepost = false + @Published public var isRepostEnabled = true + + @Published public var isLike = false @Published public var replyCount: Int = 0 @Published public var repostCount: Int = 0 @@ -87,7 +112,7 @@ extension StatusView { .share() .eraseToAnyPublisher() - public let contentRevealChangePublisher = PassthroughSubject() + // public let contentRevealChangePublisher = PassthroughSubject() public enum Header { case none @@ -101,12 +126,76 @@ extension StatusView { } init() { + // isMyself + Publishers.CombineLatest( + $authenticationContext, + $userIdentifier + ) + .map { authenticationContext, userIdentifier in + guard let authenticationContext = authenticationContext, + let userIdentifier = userIdentifier + else { return false } + return authenticationContext.userIdentifier == userIdentifier + } + .assign(to: &$isMyself) + // isContentSensitive + Publishers.CombineLatest( + $platform, + $spoilerContent + ) + .map { platform, spoilerContent in + switch platform { + case .none: return false + case .twitter: return false + case .mastodon: return spoilerContent != nil + } + } + .assign(to: &$isContentSensitive) + // isContentReveal + Publishers.CombineLatest( + $isContentSensitive, + $isContentSensitiveToggled + ) + .map { $0 ? $1 : !$1 } + .assign(to: &$isContentReveal) + // isMediaReveal Publishers.CombineLatest( $isMediaSensitive, $isMediaSensitiveToggled ) - .map { $1 ? !$0 : $0 } + .map { $0 ? $1 : !$1 } .assign(to: &$isMediaReveal) + // isRepostEnabled + Publishers.CombineLatest4( + $platform, + $visibility, + $protected, + $isMyself + ) + .map { platform, visibility, protected, isMyself in + switch platform { + case .none: + return true + case .twitter: + return isMyself ? true : !protected + case .mastodon: + if isMyself { + return true + } + switch visibility { + case .none: + return true + case .mastodon(let visibility): + switch visibility { + case .public, .unlisted: + return true + case .private, .direct, ._other: + return false + } + } + } + } + .assign(to: &$isRepostEnabled) } } } @@ -209,6 +298,9 @@ extension StatusView.ViewModel { else { return } statusView.visibilityImageView.image = image + statusView.visibilityImageView.accessibilityLabel = visibility.accessibilityLabel + statusView.visibilityImageView.accessibilityTraits = .staticText + statusView.visibilityImageView.isAccessibilityElement = true statusView.setVisibilityDisplay() } .store(in: &disposeBag) @@ -262,30 +354,23 @@ extension StatusView.ViewModel { statusView.setSpoilerDisplay() } .store(in: &disposeBag) - - Publishers.CombineLatest( - $isContentReveal, - $spoilerContent - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] isContentReveal, spoilerContent in - guard let self = self else { return } - guard spoilerContent != nil else { - // ignore reveal state when no spoiler exists - statusView.contentTextView.isHidden = false - return + $isContentReveal + .sink { isContentReveal in + statusView.contentTextView.isHidden = !isContentReveal + + let label = isContentReveal ? L10n.Accessibility.Common.Status.Actions.hideContent : L10n.Accessibility.Common.Status.Actions.revealContent + statusView.expandContentButton.accessibilityLabel = label } - - statusView.contentTextView.isHidden = !isContentReveal - self.contentRevealChangePublisher.send() - } - .store(in: &disposeBag) + .store(in: &disposeBag) $source .sink { source in statusView.metricsDashboardView.sourceLabel.text = source ?? "" } .store(in: &disposeBag) // dashboard + $platform + .assign(to: \.platform, on: statusView.metricsDashboardView.viewModel) + .store(in: &disposeBag) Publishers.CombineLatest4( $replyCount, $repostCount, @@ -295,15 +380,10 @@ extension StatusView.ViewModel { .sink { replyCount, repostCount, quoteCount, likeCount in switch statusView.style { case .plain: - statusView.setMetricsDisplay() - - statusView.metricsDashboardView.setupReply(count: replyCount) - statusView.metricsDashboardView.setupRepost(count: repostCount) - statusView.metricsDashboardView.setupQuote(count: quoteCount) - statusView.metricsDashboardView.setupLike(count: likeCount) - - let needsDashboardDisplay = replyCount > 0 || repostCount > 0 || quoteCount > 0 || likeCount > 0 - statusView.metricsDashboardView.dashboardContainer.isHidden = !needsDashboardDisplay + statusView.metricsDashboardView.viewModel.replyCount = replyCount + statusView.metricsDashboardView.viewModel.repostCount = repostCount + statusView.metricsDashboardView.viewModel.quoteCount = quoteCount + statusView.metricsDashboardView.viewModel.likeCount = likeCount default: break } @@ -351,7 +431,7 @@ extension StatusView.ViewModel { .store(in: &disposeBag) $isMediaReveal .sink { isMediaReveal in - statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = isMediaReveal + statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay = !isMediaReveal } .store(in: &disposeBag) $isMediaSensitiveSwitchable @@ -381,7 +461,7 @@ extension StatusView.ViewModel { } .store(in: &disposeBag) // poll - let pollVoteDescription = Publishers.CombineLatest( + Publishers.CombineLatest( $voterCount, $voteCount ) @@ -394,7 +474,8 @@ extension StatusView.ViewModel { } return description } - let pollCountdownDescription = Publishers.CombineLatest3( + .assign(to: &$pollVoteDescription) + Publishers.CombineLatest3( $expireAt, $expired, timestampUpdatePublisher.prepend(Date()).eraseToAnyPublisher() @@ -412,9 +493,10 @@ extension StatusView.ViewModel { return timeLeft } + .assign(to: &$pollCountdownDescription) Publishers.CombineLatest( - pollVoteDescription, - pollCountdownDescription + $pollVoteDescription, + $pollCountdownDescription ) .sink { pollVoteDescription, pollCountdownDescription in let description = [ @@ -422,9 +504,9 @@ extension StatusView.ViewModel { pollCountdownDescription ] .compactMap { $0 } - .joined(separator: " · ") - statusView.pollVoteDescriptionLabel.text = description + statusView.pollVoteDescriptionLabel.text = description.joined(separator: " · ") + statusView.pollVoteDescriptionLabel.accessibilityLabel = description.joined(separator: ", ") } .store(in: &disposeBag) Publishers.CombineLatest( @@ -451,42 +533,56 @@ extension StatusView.ViewModel { private func bindLocation(statusView: StatusView) { $location .sink { location in - guard let location = location, !location.isEmpty else { return } + guard let location = location, !location.isEmpty else { + statusView.locationLabel.isAccessibilityElement = false + return + } + statusView.locationLabel.isAccessibilityElement = true + if statusView.traitCollection.preferredContentSizeCategory > .extraLarge { statusView.locationMapPinImageView.image = Asset.ObjectTools.mappin.image } else { statusView.locationMapPinImageView.image = Asset.ObjectTools.mappinMini.image } statusView.locationLabel.text = location + statusView.locationLabel.accessibilityLabel = location + statusView.setLocationDisplay() } .store(in: &disposeBag) } private func bindToolbar(statusView: StatusView) { + // platform + $platform + .assign(to: \.platform, on: statusView.toolbar.viewModel) + .store(in: &disposeBag) + // reply $replyCount .sink { count in - statusView.toolbar.setupReply(count: count, isEnabled: true) + statusView.toolbar.setupReply(count: count, isEnabled: true) // TODO: } .store(in: &disposeBag) + // repost Publishers.CombineLatest3( - $isRepost, $repostCount, - $protected + $isRepost, + $isRepostEnabled ) - .sink { isRepost, count, protected in - statusView.toolbar.setupRepost(count: count, isRepost: isRepost, isLocked: protected) + .sink { count, isRepost, isEnabled in + statusView.toolbar.setupRepost(count: count, isEnabled: isEnabled, isHighlighted: isRepost) } .store(in: &disposeBag) + // like Publishers.CombineLatest( - $isLike, - $likeCount + $likeCount, + $isLike ) - .sink { isLike, count in - statusView.toolbar.setupLike(count: count, isLike: isLike) + .sink { count, isLike in + statusView.toolbar.setupLike(count: count, isHighlighted: isLike) } .store(in: &disposeBag) - + // menu Publishers.CombineLatest3( $sharePlaintextContent, $shareStatusURL, @@ -503,13 +599,11 @@ extension StatusView.ViewModel { } private func bindAccessibility(statusView: StatusView) { - let contentAccessibilityLabel = Publishers.CombineLatest4( + let authorAccessibilityLabel = Publishers.CombineLatest( $header, - $authorName, - $timeAgoStyleTimestamp, - $content + $authorName ) - .map { header, authorName, timestamp, content -> String? in + .map { header, authorName -> String? in var strings: [String?] = [] switch header { @@ -522,32 +616,145 @@ extension StatusView.ViewModel { } strings.append(authorName?.string) + + return strings.compactMap { $0 }.joined(separator: ", ") + } + + let metaAccessibilityLabel = Publishers.CombineLatest( + $timeAgoStyleTimestamp, + $visibility + ) + .map { timestamp, visibility -> String? in + var strings: [String?] = [] + + strings.append(visibility?.accessibilityLabel) strings.append(timestamp) - strings.append(content?.string) return strings.compactMap { $0 }.joined(separator: ", ") } - let meidaAccessibilityLabel = $mediaViewConfigurations + let contentAccessibilityLabel = Publishers.CombineLatest4( + $platform, + $isContentReveal, + $spoilerContent, + $content + ) + .map { platform, isContentReveal, spoilerContent, content -> String? in + var strings: [String?] = [] + switch platform { + case .none: + break + case .twitter: + strings.append(content?.string) + case .mastodon: + if let spoilerContent = spoilerContent?.string { + strings.append(L10n.Accessibility.Common.Status.contentWarning) + strings.append(spoilerContent) + } + if isContentReveal { + strings.append(content?.string) + } + } + + return strings.compactMap { $0 }.joined(separator: ", ") + } + + let mediaAccessibilityLabel = $mediaViewConfigurations .map { configurations -> String? in let count = configurations.count - // TODO: i18n - return count > 0 ? "\(count) media" : nil + return count > 0 ? L10n.Count.media(count) : nil + } + + let toolbarAccessibilityLabel = Publishers.CombineLatest3( + $platform, + $isRepost, + $isLike + ) + .map { platform, isRepost, isLike -> String? in + var strings: [String?] = [] + + switch platform { + case .none: + break + case .twitter: + if isRepost { + strings.append(L10n.Accessibility.Common.Status.retweeted) + } + case .mastodon: + if isRepost { + strings.append(L10n.Accessibility.Common.Status.boosted) + } } - // TODO: Toolbar + if isLike { + strings.append(L10n.Accessibility.Common.Status.liked) + } + + return strings.compactMap { $0 }.joined(separator: ", ") + } - Publishers.CombineLatest( + let pollAccessibilityLabel = Publishers.CombineLatest3( + $pollItems, + $pollVoteDescription, + $pollCountdownDescription + ) + .map { items, pollVoteDescription, pollCountdownDescription -> String? in + guard !items.isEmpty else { return nil } + guard let managedObjectContext = self.managedObjectContext else { return nil } + + var strings: [String?] = [] + + let ordinalPrefix = L10n.Accessibility.Common.Status.pollOptionOrdinalPrefix + + for (i, item) in items.enumerated() { + switch item { + case .option(let record): + guard let option = record.object(in: managedObjectContext) else { continue } + let number = NSNumber(value: i + 1) + guard let ordinal = StatusView.ViewModel.pollOptionOrdinalNumberFormatter.string(from: number) else { break } + strings.append("\(ordinalPrefix), \(ordinal), \(option.title)") + + if option.isSelected { + strings.append(L10n.Accessibility.VoiceOver.selected) + } + } + } + + strings.append(pollVoteDescription) + pollCountdownDescription.flatMap { strings.append($0) } + + return strings.compactMap { $0 }.joined(separator: ", ") + } + + let groupOne = Publishers.CombineLatest4( + authorAccessibilityLabel, + metaAccessibilityLabel, contentAccessibilityLabel, - meidaAccessibilityLabel + mediaAccessibilityLabel ) - .map { content, media in - let group = [ - content, - media - ] + .map { a, b, c, d -> String? in + return [a, b, c, d] + .compactMap { $0 } + .joined(separator: ", ") + } + + let groupTwo = Publishers.CombineLatest3( + pollAccessibilityLabel, + $location, + toolbarAccessibilityLabel + ) + .map { a, b, c -> String? in + return [a, b, c] + .compactMap { $0 } + .joined(separator: ", ") + } - return group + Publishers.CombineLatest( + groupOne, + groupTwo + ) + .map { a, b -> String in + return [a, b] .compactMap { $0 } .joined(separator: ", ") } @@ -558,6 +765,14 @@ extension StatusView.ViewModel { statusView.accessibilityLabel = accessibilityLabel } .store(in: &disposeBag) + + // poll + $pollItems + .sink { items in + statusView.pollVoteDescriptionLabel.isAccessibilityElement = !items.isEmpty + statusView.pollVoteButton.isAccessibilityElement = !items.isEmpty + } + .store(in: &disposeBag) } } @@ -566,22 +781,25 @@ extension StatusView { public struct ConfigurationContext { public let dateTimeProvider: DateTimeProvider public let twitterTextProvider: TwitterTextProvider - public let activeAuthenticationContext: AnyPublisher + public let authenticationContext: Published.Publisher public init( dateTimeProvider: DateTimeProvider, twitterTextProvider: TwitterTextProvider, - activeAuthenticationContext: AnyPublisher + authenticationContext: Published.Publisher ) { self.dateTimeProvider = dateTimeProvider self.twitterTextProvider = twitterTextProvider - self.activeAuthenticationContext = activeAuthenticationContext + self.authenticationContext = authenticationContext } } } extension StatusView { - public func configure(feed: Feed, configurationContext: ConfigurationContext) { + public func configure( + feed: Feed, + configurationContext: ConfigurationContext + ) { switch feed.content { case .none: logger.log(level: .info, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Warning] feed content missing") @@ -637,23 +855,20 @@ extension StatusView { twitterStatus status: TwitterStatus, configurationContext: ConfigurationContext ) { + viewModel.managedObjectContext = status.managedObjectContext viewModel.objects.insert(status) + viewModel.platform = .twitter + viewModel.dateTimeProvider = configurationContext.dateTimeProvider + viewModel.twitterTextProvider = configurationContext.twitterTextProvider + configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) + configureHeader(twitterStatus: status) - configureAuthor( - twitterStatus: status, - dateTimeProvider: configurationContext.dateTimeProvider - ) - configureContent( - twitterStatus: status, - twitterTextProvider: configurationContext.twitterTextProvider - ) + configureAuthor(twitterStatus: status) + configureContent(twitterStatus: status) configureMedia(twitterStatus: status) configureLocation(twitterStatus: status) - configureToolbar( - twitterStatus: status, - activeAuthenticationContext: configurationContext.activeAuthenticationContext - ) + configureToolbar(twitterStatus: status) if let quote = status.quote { quoteStatusView?.configure( @@ -680,11 +895,16 @@ extension StatusView { } } - private func configureAuthor( - twitterStatus status: TwitterStatus, - dateTimeProvider: DateTimeProvider - ) { + private func configureAuthor(twitterStatus status: TwitterStatus) { + guard let dateTimeProvider = viewModel.dateTimeProvider else { + assertionFailure() + return + } + let author = (status.repost ?? status).author + + viewModel.userIdentifier = .twitter(.init(id: author.id)) + // author avatar author.publisher(for: \.profileImageURL) .map { _ in author.avatarImageURL() } @@ -712,10 +932,12 @@ extension StatusView { .store(in: &disposeBag) } - private func configureContent( - twitterStatus status: TwitterStatus, - twitterTextProvider: TwitterTextProvider - ) { + private func configureContent(twitterStatus status: TwitterStatus) { + guard let twitterTextProvider = viewModel.twitterTextProvider else { + assertionFailure() + return + } + let status = status.repost ?? status let content = TwitterContent(content: status.displayText) let metaContent = TwitterMetaContent.convert( @@ -723,8 +945,10 @@ extension StatusView { urlMaximumLength: 20, twitterTextProvider: twitterTextProvider ) - viewModel.isContentReveal = true viewModel.spoilerContent = nil + viewModel.isContentReveal = true + viewModel.isContentSensitive = false + viewModel.isContentSensitiveToggled = false viewModel.content = metaContent viewModel.sharePlaintextContent = status.displayText viewModel.source = status.source @@ -750,11 +974,9 @@ extension StatusView { .store(in: &disposeBag) } - private func configureToolbar( - twitterStatus status: TwitterStatus, - activeAuthenticationContext: AnyPublisher - ) { + private func configureToolbar(twitterStatus status: TwitterStatus) { let status = status.repost ?? status + status.publisher(for: \.replyCount) .map(Int.init) .assign(to: \.replyCount, on: viewModel) @@ -763,6 +985,10 @@ extension StatusView { .map(Int.init) .assign(to: \.repostCount, on: viewModel) .store(in: &disposeBag) + status.publisher(for: \.quoteCount) + .map(Int.init) + .assign(to: \.quoteCount, on: viewModel) + .store(in: &disposeBag) status.publisher(for: \.likeCount) .map(Int.init) .assign(to: \.likeCount, on: viewModel) @@ -771,7 +997,7 @@ extension StatusView { // relationship Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.repostBy) ) .map { authenticationContext, repostBy in @@ -785,7 +1011,7 @@ extension StatusView { .store(in: &disposeBag) Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.likeBy) ) .map { authenticationContext, likeBy in @@ -799,7 +1025,7 @@ extension StatusView { .store(in: &disposeBag) let authorUserID = status.author.id - activeAuthenticationContext + viewModel.$authenticationContext .map { authenticationContext in guard let authenticationContext = authenticationContext?.twitterAuthenticationContext else { return false @@ -818,14 +1044,20 @@ extension StatusView { notification: MastodonNotification?, configurationContext: ConfigurationContext ) { + viewModel.managedObjectContext = status.managedObjectContext viewModel.objects.insert(status) + viewModel.platform = .mastodon + viewModel.dateTimeProvider = configurationContext.dateTimeProvider + viewModel.twitterTextProvider = configurationContext.twitterTextProvider + configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) + configureHeader(mastodonStatus: status, mastodonNotification: notification) - configureAuthor(mastodonStatus: status, dateTimeProvider: configurationContext.dateTimeProvider) + configureAuthor(mastodonStatus: status) configureContent(mastodonStatus: status) configureMedia(mastodonStatus: status) - configurePoll(mastodonStatus: status, activeAuthenticationContext: configurationContext.activeAuthenticationContext) - configureToolbar(mastodonStatus: status, activeAuthenticationContext: configurationContext.activeAuthenticationContext) + configurePoll(mastodonStatus: status) + configureToolbar(mastodonStatus: status) } private func configureHeader( @@ -872,11 +1104,11 @@ extension StatusView { } } - private func configureAuthor( - mastodonStatus status: MastodonStatus, - dateTimeProvider: DateTimeProvider - ) { + private func configureAuthor(mastodonStatus status: MastodonStatus) { let author = (status.repost ?? status).author + + viewModel.userIdentifier = .mastodon(.init(domain: author.domain, id: author.id)) + // author avatar author.publisher(for: \.avatar) .map { url in url.flatMap { URL(string: $0) } } @@ -911,7 +1143,6 @@ extension StatusView { // visibility viewModel.visibility = status.visibility.asStatusVisibility // timestamp - viewModel.dateTimeProvider = dateTimeProvider (status.repost ?? status).publisher(for: \.createdAt) .map { $0 as Date? } .assign(to: \.timestamp, on: viewModel) @@ -943,8 +1174,9 @@ extension StatusView { viewModel.spoilerContent = nil } - status.publisher(for: \.isContentReveal) - .assign(to: \.isContentReveal, on: viewModel) + viewModel.isContentSensitiveToggled = status.isContentSensitiveToggled + status.publisher(for: \.isContentSensitiveToggled) + .assign(to: \.isContentSensitiveToggled, on: viewModel) .store(in: &disposeBag) viewModel.source = status.source @@ -968,21 +1200,13 @@ extension StatusView { animated: false ) - status.publisher(for: \.isMediaSensitive) - .receive(on: DispatchQueue.main) - .assign(to: \.isMediaSensitive, on: viewModel) - .store(in: &disposeBag) - status.publisher(for: \.isMediaSensitiveToggled) .receive(on: DispatchQueue.main) .assign(to: \.isMediaSensitiveToggled, on: viewModel) .store(in: &disposeBag) } - private func configurePoll( - mastodonStatus status: MastodonStatus, - activeAuthenticationContext: AnyPublisher - ) { + private func configurePoll(mastodonStatus status: MastodonStatus) { let status = status.repost ?? status // pollItems @@ -1014,7 +1238,7 @@ extension StatusView { Publishers.CombineLatest3( poll.publisher(for: \.voteBy), poll.publisher(for: \.expired), - activeAuthenticationContext + viewModel.$authenticationContext ) .map { voteBy, expired, authenticationContext in guard case let .mastodon(authenticationContext) = authenticationContext else { return false } @@ -1049,12 +1273,10 @@ extension StatusView { .store(in: &disposeBag) } - private func configureToolbar( - mastodonStatus status: MastodonStatus, - activeAuthenticationContext: AnyPublisher - ) { + private func configureToolbar(mastodonStatus status: MastodonStatus) { let status = status.repost ?? status + viewModel.quoteCount = 0 status.publisher(for: \.replyCount) .map(Int.init) .assign(to: \.replyCount, on: viewModel) @@ -1071,7 +1293,7 @@ extension StatusView { // relationship Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.repostBy) ) .map { authenticationContext, repostBy in @@ -1086,7 +1308,7 @@ extension StatusView { .store(in: &disposeBag) Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.likeBy) ) .map { authenticationContext, likeBy in @@ -1101,7 +1323,7 @@ extension StatusView { .store(in: &disposeBag) let authorUserID = status.author.id - activeAuthenticationContext + viewModel.$authenticationContext .map { authenticationContext in guard let authenticationContext = authenticationContext?.mastodonAuthenticationContext else { return false diff --git a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift index 94f4e38a..c6107955 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/StatusView.swift @@ -52,7 +52,7 @@ public final class StatusView: UIView { public static let bodyContainerStackViewSpacing: CGFloat = 10 public static let quoteStatusViewContainerLayoutMargin: CGFloat = 12 - let logger = Logger(subsystem: "StatusView", category: "UI") + let logger = Logger(subsystem: "StatusView", category: "View") public private(set) var style: Style? @@ -74,14 +74,22 @@ public final class StatusView: UIView { public let headerContainerView = UIView() public let headerIconImageView = UIImageView() public static var headerTextLabelStyle: TextStyle { .statusHeader } - public let headerTextLabel = MetaLabel(style: .statusHeader) + public let headerTextLabel: MetaLabel = { + let label = MetaLabel(style: .statusHeader) + label.isUserInteractionEnabled = false + return label + }() // avatar public let authorAvatarButton = AvatarButton() // author public static var authorNameLabelStyle: TextStyle { .statusAuthorName } - public let authorNameLabel = MetaLabel(style: StatusView.authorNameLabelStyle) + public let authorNameLabel: MetaLabel = { + let label = MetaLabel(style: StatusView.authorNameLabelStyle) + label.accessibilityTraits = .staticText + return label + }() public let lockImageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = .secondaryLabel @@ -257,12 +265,12 @@ extension StatusView { ]) // header - headerTextLabel.isUserInteractionEnabled = false let headerTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer headerTapGestureRecognizer.addTarget(self, action: #selector(StatusView.headerTapGestureRecognizerHandler(_:))) headerContainerView.addGestureRecognizer(headerTapGestureRecognizer) // avatar button - authorAvatarButton.accessibilityLabel = "Open Author Profile" // TODO: i18n + authorAvatarButton.accessibilityLabel = L10n.Accessibility.Common.Status.authorAvatar + authorAvatarButton.accessibilityHint = L10n.Accessibility.VoiceOver.doubleTapToOpenProfile authorAvatarButton.addTarget(self, action: #selector(StatusView.authorAvatarButtonDidPressed(_:)), for: .touchUpInside) // expand content expandContentButton.addTarget(self, action: #selector(StatusView.expandContentButtonDidPressed(_:)), for: .touchUpInside) @@ -329,7 +337,6 @@ extension StatusView { statusView.pollVoteActivityIndicatorView.isHidden = true statusView.quoteStatusView?.isHidden = true statusView.locationContainer.isHidden = true - statusView.metricsDashboardView.isHidden = true } } } @@ -904,10 +911,6 @@ extension StatusView { locationContainer.isHidden = false } - public func setMetricsDisplay() { - metricsDashboardView.isHidden = false - } - // content text Width public var contentMaxLayoutWidth: CGFloat { let inset = contentLayoutInset diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift index 06f47e36..27679b6f 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView+ViewModel.swift @@ -31,7 +31,7 @@ extension UserView { // TODO: verified | bot @Published public var name: MetaContent? = PlaintextMetaContent(string: " ") - @Published public var username: String? = " " + @Published public var username: String? @Published public var protected: Bool = false @@ -68,20 +68,19 @@ extension UserView.ViewModel { case .none: userView.authorProfileAvatarView.badge = .none case .platform: - let badge: UIImage? = { + userView.authorProfileAvatarView.badge = { switch platform { - case .none: return nil - case .twitter: return Asset.Badge.circleTwitter.image - case .mastodon: return Asset.Badge.circleMastodon.image + case .none: return .none + case .twitter: return .circle(.twitter) + case .mastodon: return .circle(.mastodon) } }() - userView.authorProfileAvatarView.badge = badge != nil ? .circle : .none - userView.authorProfileAvatarView.badgeImageView.image = badge case .user: userView.authorProfileAvatarView.badge = .none } } .store(in: &disposeBag) + // badge UserDefaults.shared .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in let avatarStyle = defaults.avatarStyle @@ -98,21 +97,6 @@ extension UserView.ViewModel { animator.startAnimation() } .store(in: &observations) -// // badge -// $platform -// .sink { platform in -// switch platform { -// case .twitter: -// userView.badgeImageView.image = Asset.Badge.twitter.image -// userView.setBadgeDisplay() -// case .mastodon: -// userView.badgeImageView.image = Asset.Badge.mastodon.image -// userView.setBadgeDisplay() -// case .none: -// break -// } -// } -// .store(in: &disposeBag) // header $header .sink { header in @@ -140,6 +124,9 @@ extension UserView.ViewModel { .store(in: &disposeBag) // username $username + .map { username in + return username.flatMap { "@\($0)" } ?? " " + } .assign(to: \.text, on: userView.usernameLabel) .store(in: &disposeBag) // protected @@ -235,7 +222,7 @@ extension UserView { .store(in: &disposeBag) // author username user.publisher(for: \.username) - .map { "@\($0)" as String? } + .map { $0 as String? } .assign(to: \.username, on: viewModel) .store(in: &disposeBag) // protected @@ -276,8 +263,8 @@ extension UserView { .assign(to: \.name, on: viewModel) .store(in: &disposeBag) // author username - user.publisher(for: \.username) - .map { "@\($0)" as String? } + user.publisher(for: \.acct) + .map { _ in user.acctWithDomain as String? } .assign(to: \.username, on: viewModel) .store(in: &disposeBag) // protected diff --git a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift index f059a81d..73bd1593 100644 --- a/TwidereSDK/Sources/TwidereUI/Content/UserView.swift +++ b/TwidereSDK/Sources/TwidereUI/Content/UserView.swift @@ -72,7 +72,11 @@ public final class UserView: UIView { public let headerTextLabel = MetaLabel(style: .statusHeader) // avatar - public let authorProfileAvatarView = ProfileAvatarView() + public let authorProfileAvatarView: ProfileAvatarView = { + let profileAvatarView = ProfileAvatarView() + profileAvatarView.setup(dimension: .inline) + return profileAvatarView + }() // name public let nameLabel = MetaLabel(style: .userAuthorName) @@ -166,8 +170,12 @@ extension UserView { containerStackView.addArrangedSubview(contentStackView) // content: H - [ user avatar | info container | accessory container ] - authorProfileAvatarView.dimension = UserView.avatarImageViewSize.width + authorProfileAvatarView.translatesAutoresizingMaskIntoConstraints = false contentStackView.addArrangedSubview(authorProfileAvatarView) + NSLayoutConstraint.activate([ + authorProfileAvatarView.widthAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.width).priority(.required - 1), + authorProfileAvatarView.heightAnchor.constraint(equalToConstant: UserView.avatarImageViewSize.height).priority(.required - 1), + ]) contentStackView.addArrangedSubview(infoContainerStackView) contentStackView.addArrangedSubview(accessoryContainerView) diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/PollOptionTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/PollOptionTableViewCell.swift index 9a3665fe..9e4078d9 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/PollOptionTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/PollOptionTableViewCell.swift @@ -54,7 +54,12 @@ extension PollOptionTableViewCell { contentView.bottomAnchor.constraint(equalTo: optionView.bottomAnchor, constant: PollOptionTableViewCell.margin), optionView.heightAnchor.constraint(equalToConstant: PollOptionView.height).priority(.required - 1), ]) - optionView.setup(style: .plain) + optionView.setup(style: .plain) + + // accessibility + accessibilityElements = [optionView] + optionView.isAccessibilityElement = true + } } diff --git a/TwidereSDK/Sources/TwidereUI/TableViewCell/UserTableViewCell.swift b/TwidereSDK/Sources/TwidereUI/TableViewCell/UserTableViewCell.swift index 1ce407a2..704b43c3 100644 --- a/TwidereSDK/Sources/TwidereUI/TableViewCell/UserTableViewCell.swift +++ b/TwidereSDK/Sources/TwidereUI/TableViewCell/UserTableViewCell.swift @@ -18,7 +18,7 @@ public class UserTableViewCell: UITableViewCell { var disposeBag = Set() - let logger = Logger(subsystem: "UserTableViewCell", category: "UI") + let logger = Logger(subsystem: "UserTableViewCell", category: "View") public let userView = UserView() diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift new file mode 100644 index 00000000..f581b33d --- /dev/null +++ b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar+ViewModel.swift @@ -0,0 +1,243 @@ +// +// StatusToolbar+ViewModel.swift +// +// +// Created by MainasuK on 2022-2-23. +// + +import UIKit +import Combine +import CoreDataStack +import TwidereAsset + +extension StatusToolbar { + public final class ViewModel: ObservableObject { + var disposeBag = Set() + + @Published public var traitCollectionDidChange = CurrentValueSubject(Void()) + @Published public var platform: Platform = .none + + @Published public var replyCount: Int = 0 + @Published public var isReplyEnabled = true + + @Published public var repostCount: Int = 0 + @Published public var isRepostEnabled = true + @Published public var isRepostHighlighted = true + + @Published public var likeCount: Int = 0 + // @Published public var isLikeEnabled = true + @Published public var isLikeHighlighted = true + + func bind(view: StatusToolbar) { + // reply + Publishers.CombineLatest( + $replyCount, + $isReplyEnabled + ) + .sink { count, isEnabled in + // title + let text = ViewModel.metricText(count: count) + switch view.style { + case .none: + break + case .inline: + view.replyButton.setTitle(text, for: .normal) + view.replyButton.accessibilityHint = L10n.Count.reply(count) + case .plain: + view.replyButton.accessibilityHint = nil + } + view.replyButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.reply + + // isEnabled + view.replyButton.isEnabled = isEnabled + } + .store(in: &disposeBag) + // repost + Publishers.CombineLatest3( + $repostCount, + $isRepostEnabled, + $platform + ) + .sink { count, isEnabled, platform in + // title + let text = ViewModel.metricText(count: count) + switch view.style { + case .none: + break + case .inline: + view.repostButton.setTitle(text, for: .normal) + view.repostButton.accessibilityHint = L10n.Count.reblog(count) + case .plain: + view.repostButton.accessibilityHint = nil + } + + switch platform { + case .none: + view.repostButton.accessibilityLabel = nil + case .twitter: + view.repostButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.retweet + case .mastodon: + view.repostButton.accessibilityLabel = L10n.Common.Controls.Status.Actions.boost + } + + // isEnabled + view.repostButton.isEnabled = isEnabled + } + .store(in: &disposeBag) + Publishers.CombineLatest( + $isRepostHighlighted, + $traitCollectionDidChange + ) + .sink { isHighlighted, _ in + // isHighlighted + let tintColor = isHighlighted ? Asset.Scene.Status.Toolbar.repost.color : .secondaryLabel + view.repostButton.tintColor = tintColor + view.repostButton.setTitleColor(tintColor, for: .normal) + view.repostButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) + if isHighlighted { + view.repostButton.accessibilityTraits.insert(.selected) + } else { + view.repostButton.accessibilityTraits.remove(.selected) + } + } + .store(in: &disposeBag) + // like + $likeCount + .sink { count in + // title + let text = ViewModel.metricText(count: count) + switch view.style { + case .none: + break + case .inline: + view.likeButton.setTitle(text, for: .normal) + view.likeButton.accessibilityHint = L10n.Count.reply(count) + case .plain: + view.likeButton.accessibilityHint = nil + // no titile + } + view.likeButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.like + } + .store(in: &disposeBag) + Publishers.CombineLatest( + $isLikeHighlighted, + $traitCollectionDidChange + ) + .sink { isHighlighted, _ in + // isHighlighted + let tintColor = isHighlighted ? Asset.Scene.Status.Toolbar.like.color : .secondaryLabel + view.likeButton.tintColor = tintColor + view.likeButton.setTitleColor(tintColor, for: .normal) + view.likeButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) + switch view.style { + case .none: + break + case .inline: + let image: UIImage = isHighlighted ? Asset.Health.heartFillMini.image : Asset.Health.heartMini.image + view.likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) + case .plain: + let image: UIImage = isHighlighted ? Asset.Health.heartFill.image : Asset.Health.heart.image + view.likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) + } + if isHighlighted { + view.likeButton.accessibilityTraits.insert(.selected) + } else { + view.likeButton.accessibilityTraits.remove(.selected) + } + } + .store(in: &disposeBag) + } + + private static func metricText(count: Int) -> String { + guard count > 0 else { return "" } + return StatusToolbar.numberMetricFormatter.string(from: count) ?? "" + } + } +} + +extension StatusToolbar { + + public func setupReply(count: Int, isEnabled: Bool) { + viewModel.replyCount = count + viewModel.isReplyEnabled = isEnabled + } + + public func setupRepost(count: Int, isEnabled: Bool, isHighlighted: Bool) { + viewModel.repostCount = count + viewModel.isRepostEnabled = isEnabled + viewModel.isRepostHighlighted = isHighlighted + } + + public func setupLike(count: Int, isHighlighted: Bool) { + viewModel.likeCount = count + viewModel.isLikeHighlighted = isHighlighted + } + + public struct MenuContext { + let shareText: String? + let shareLink: String? + let displayDeleteAction: Bool + } + + public func setupMenu(menuContext: MenuContext) { + menuButton.menu = { + var children: [UIMenuElement] = [ + UIAction( + title: L10n.Common.Controls.Status.Actions.copyText.capitalized, + image: UIImage(systemName: "doc.on.doc"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off + ) { _ in + guard let text = menuContext.shareText else { return } + UIPasteboard.general.string = text + }, + UIAction( + title: L10n.Common.Controls.Status.Actions.copyLink.capitalized, + image: UIImage(systemName: "link"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off + ) { _ in + guard let text = menuContext.shareLink else { return } + UIPasteboard.general.string = text + }, + UIAction( + title: L10n.Common.Controls.Status.Actions.shareLink.capitalized, + image: UIImage(systemName: "square.and.arrow.up"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off + ) { [weak self] _ in + guard let self = self else { return } + self.delegate?.statusToolbar(self, actionDidPressed: .menu, button: self.menuButton) + } + ] + + if menuContext.displayDeleteAction { + let removeAction = UIAction( + title: L10n.Common.Controls.Actions.delete, + image: UIImage(systemName: "minus.circle"), + identifier: nil, + discoverabilityTitle: nil, + attributes: .destructive, + state: .off + ) { [weak self] _ in + guard let self = self else { return } + self.delegate?.statusToolbar(self, menuActionDidPressed: .remove, menuButton: self.menuButton) + } + children.append(removeAction) + } + + return UIMenu(title: "", options: [], children: children) + }() + + + menuButton.showsMenuAsPrimaryAction = true + menuButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.menu + } + +} diff --git a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift index 649c9a6b..a5ab601a 100644 --- a/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift +++ b/TwidereSDK/Sources/TwidereUI/Toolbar/StatusToolbar.swift @@ -19,16 +19,23 @@ public final class StatusToolbar: UIView { public static let numberMetricFormatter = NumberMetricFormatter() + public weak var delegate: StatusToolbarDelegate? + + public private(set) lazy var viewModel: ViewModel = { + let viewModel = ViewModel() + viewModel.bind(view: self) + return viewModel + }() + + private let logger = Logger(subsystem: "StatusToolbar", category: "Toolbar") + private let container = UIStackView() + private(set) var style: Style? + public let replyButton = HitTestExpandedButton() public let repostButton = HitTestExpandedButton() public let likeButton = HitTestExpandedButton() public let menuButton = HitTestExpandedButton() - private let logger = Logger(subsystem: "StatusToolbar", category: "UI") - private let container = UIStackView() - private var style: Style? - - public weak var delegate: StatusToolbarDelegate? public override init(frame: CGRect) { super.init(frame: frame) @@ -58,6 +65,17 @@ extension StatusToolbar { container.trailingAnchor.constraint(equalTo: trailingAnchor), container.bottomAnchor.constraint(equalTo: bottomAnchor), ]) + + replyButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.reply + // dynamic label for repostButton + likeButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.like + menuButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.menu + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + viewModel.traitCollectionDidChange.send() } public func setup(style: Style) { @@ -185,151 +203,3 @@ extension StatusToolbar { } } - -extension StatusToolbar { - - private func metricText(count: Int) -> String { - guard count > 0 else { return "" } - return StatusToolbar.numberMetricFormatter.string(from: count) ?? "" - } - - public func setupReply(count: Int, isEnabled: Bool) { - let text = metricText(count: count) - switch style { - case .inline: - replyButton.setTitle(text, for: .normal) - case .plain: - break - case .none: - break - } - - replyButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.reply - } - - public func setupRepost(count: Int, isRepost: Bool, isLocked: Bool) { - // set title - let text = metricText(count: count) - switch style { - case .inline: - repostButton.setTitle(text, for: .normal) - case .plain: - break - case .none: - break - } - - // set color - let tintColor = isRepost ? Asset.Scene.Status.Toolbar.repost.color : .secondaryLabel - repostButton.tintColor = tintColor - repostButton.setTitleColor(tintColor, for: .normal) - repostButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) - - // TODO: loked - - repostButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.retweet - if isRepost { - repostButton.accessibilityTraits.insert(.selected) - } else { - repostButton.accessibilityTraits.remove(.selected) - } - } - - public func setupLike(count: Int, isLike: Bool) { - // set title - let text = metricText(count: count) - switch style { - case .inline: - let image: UIImage = isLike ? Asset.Health.heartFillMini.image : Asset.Health.heartMini.image - likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) - likeButton.setTitle(text, for: .normal) - case .plain: - let image: UIImage = isLike ? Asset.Health.heartFill.image : Asset.Health.heart.image - likeButton.setImage(image.withRenderingMode(.alwaysTemplate), for: .normal) - // no title - case .none: - break - } - - // set color - let tintColor = isLike ? Asset.Scene.Status.Toolbar.like.color : .secondaryLabel - likeButton.tintColor = tintColor - likeButton.setTitleColor(tintColor, for: .normal) - likeButton.setTitleColor(tintColor.withAlphaComponent(0.8), for: .highlighted) - - likeButton.accessibilityLabel = L10n.Accessibility.Common.Status.Actions.like - if isLike { - likeButton.accessibilityTraits.insert(.selected) - } else { - likeButton.accessibilityTraits.remove(.selected) - } - } - - public struct MenuContext { - let shareText: String? - let shareLink: String? - let displayDeleteAction: Bool - } - - public func setupMenu(menuContext: MenuContext) { - menuButton.menu = { - var children: [UIMenuElement] = [ - UIAction( - title: L10n.Common.Controls.Status.Actions.copyText.capitalized, - image: UIImage(systemName: "doc.on.doc"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { _ in - guard let text = menuContext.shareText else { return } - UIPasteboard.general.string = text - }, - UIAction( - title: L10n.Common.Controls.Status.Actions.copyLink.capitalized, - image: UIImage(systemName: "link"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { _ in - guard let text = menuContext.shareLink else { return } - UIPasteboard.general.string = text - }, - UIAction( - title: L10n.Common.Controls.Status.Actions.shareLink.capitalized, - image: UIImage(systemName: "square.and.arrow.up"), - identifier: nil, - discoverabilityTitle: nil, - attributes: [], - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, actionDidPressed: .menu, button: self.menuButton) - } - ] - - if menuContext.displayDeleteAction { - let removeAction = UIAction( - title: L10n.Common.Controls.Actions.delete, - image: UIImage(systemName: "minus.circle"), - identifier: nil, - discoverabilityTitle: nil, - attributes: .destructive, - state: .off - ) { [weak self] _ in - guard let self = self else { return } - self.delegate?.statusToolbar(self, menuActionDidPressed: .remove, menuButton: self.menuButton) - } - children.append(removeAction) - } - - return UIMenu(title: "", options: [], children: children) - }() - - - menuButton.showsMenuAsPrimaryAction = true - menuButton.accessibilityLabel = "Menu" // TODO: i18n - } - -} diff --git a/TwidereX.xcodeproj/project.pbxproj b/TwidereX.xcodeproj/project.pbxproj index 2a06885e..1a7039c5 100644 --- a/TwidereX.xcodeproj/project.pbxproj +++ b/TwidereX.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ AF66F7A022E93DE9E329F6A3 /* Pods_TwidereXTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62DC6A6809A00EA057395C07 /* Pods_TwidereXTests.framework */; }; DB004BF626CE4A7F00F5C574 /* StatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB004BF526CE4A7F00F5C574 /* StatusCollectionViewCell.swift */; }; DB01091526E5EB64005F67D7 /* MastodonStatusThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01091426E5EB64005F67D7 /* MastodonStatusThreadViewModel.swift */; }; - DB01091A26E60693005F67D7 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AC0FF25401C2700E636BE /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; DB01092026E60756005F67D7 /* DataSourceFacade+StatusThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01091F26E60756005F67D7 /* DataSourceFacade+StatusThread.swift */; }; DB01092226E608B7005F67D7 /* DataSourceFacade+Repost.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01092126E608B7005F67D7 /* DataSourceFacade+Repost.swift */; }; DB01092426E6199C005F67D7 /* DataSourceProvider+StatusViewTableViewCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01092326E6199C005F67D7 /* DataSourceProvider+StatusViewTableViewCellDelegate.swift */; }; @@ -83,7 +82,6 @@ DB2EBBF0255D368200956CAA /* TableViewEntryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2EBBEF255D368200956CAA /* TableViewEntryRow.swift */; }; DB2FFF2E258B1077003DBC19 /* MosaicPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FFF2D258B1077003DBC19 /* MosaicPlayerView.swift */; }; DB2FFF3E258B78B0003DBC19 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FFF3D258B78B0003DBC19 /* AVPlayer.swift */; }; - DB2FFF61258CB051003DBC19 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FFF60258CB051003DBC19 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; }; DB30ADD626CF583900B2D2BE /* FLAnimatedImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB30ADD526CF583900B2D2BE /* FLAnimatedImage */; }; DB30ADDC26CFC7EE00B2D2BE /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */; }; DB30ADDD26CFD3CC00B2D2BE /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB2513D2599DBAB0064A876 /* HomeTimelineViewController+DebugAction.swift */; }; @@ -292,7 +290,6 @@ DBCC7AE6274BA10100E0986D /* OfficialTwitterTextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01091B26E606BB005F67D7 /* OfficialTwitterTextProvider.swift */; }; DBCE2E912591A06300926D09 /* UINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCE2E902591A06300926D09 /* UINavigationController.swift */; }; DBCE2E992591A44000926D09 /* UIViewAnimatingPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCE2E982591A44000926D09 /* UIViewAnimatingPosition.swift */; }; - DBCE2EE32593319000926D09 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCE2EE22593319000926D09 /* AvatarConfigurableView.swift */; }; DBD0B4972758B57F0015A388 /* DrawerSidebarTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEFDC91255920060086F268 /* DrawerSidebarTransitionController.swift */; }; DBD0B4982758B58F0015A388 /* DrawerSidebarPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFCC46225668B860016698E /* DrawerSidebarPresentationController.swift */; }; DBD0B4992758B58F0015A388 /* DrawerSidebarAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB761E3E25592AA20050DC01 /* DrawerSidebarAnimatedTransitioning.swift */; }; @@ -323,6 +320,7 @@ DBEA4F842511F7460007FEC5 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = DBEA4F832511F7460007FEC5 /* Kanna */; }; DBEA4F8F251314640007FEC5 /* MosaicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBEA4F8E251314640007FEC5 /* MosaicImageView.swift */; }; DBED96D8253F5D7800C5383A /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBED96D7253F5D7800C5383A /* NamingState.swift */; }; + DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */; }; DBF3309125B96E0B00A678FB /* WKNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3309025B96E0B00A678FB /* WKNavigationDelegateShim.swift */; }; DBF3309925B988A500A678FB /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DBF3309825B988A500A678FB /* Settings.bundle */; }; DBF639EA259B11F6009E12C8 /* UserTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF639E9259B11F6009E12C8 /* UserTimelineViewModel+Diffable.swift */; }; @@ -527,7 +525,6 @@ DB2FFF2D258B1077003DBC19 /* MosaicPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicPlayerView.swift; sourceTree = ""; }; DB2FFF35258B56E5003DBC19 /* VideoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlaybackService.swift; sourceTree = ""; }; DB2FFF3D258B78B0003DBC19 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; - DB2FFF60258CB051003DBC19 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; DB30ADDB26CFC7EE00B2D2BE /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; DB33017525A41102003552B0 /* StatusProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusProviderFacade.swift; sourceTree = ""; }; DB33A4A825A319A0003CED7D /* ActionToolbarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolbarContainer.swift; sourceTree = ""; }; @@ -560,7 +557,6 @@ DB434882251DF18C005B599F /* ProfileBannerInfoActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBannerInfoActionView.swift; sourceTree = ""; }; DB434887251DFE2D005B599F /* ProfileBannerStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBannerStatusView.swift; sourceTree = ""; }; DB43488C251DFF0F005B599F /* ProfileBannerStatusItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBannerStatusItemView.swift; sourceTree = ""; }; - DB43CBDC2595CE2100D47607 /* LoadMoreConfigurableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableViewContainer.swift; sourceTree = ""; }; DB44A56126C4FEAB004C8B78 /* WelcomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewModel.swift; sourceTree = ""; }; DB4B599325DFB4D60012F495 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; DB4B59B425DFBDB80012F495 /* APIService+Persist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist.swift"; sourceTree = ""; }; @@ -682,7 +678,6 @@ DB89F58B257A2A57005ECD04 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; DB89F590257A2A70005ECD04 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; DB8AC0F725401BA200E636BE /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; - DB8AC0FF25401C2700E636BE /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; DB8AC10D2540501600E636BE /* ComposeTweetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTweetViewController.swift; sourceTree = ""; }; DB8AC1462541677900E636BE /* ComposeTweetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTweetViewModel.swift; sourceTree = ""; }; DB8AC14B254167BE00E636BE /* TweetToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TweetToolbarView.swift; sourceTree = ""; }; @@ -798,7 +793,6 @@ DBCE2EB12591F38100926D09 /* FriendshipListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendshipListViewModel.swift; sourceTree = ""; }; DBCE2EC72591F74300926D09 /* FollowingListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+Diffable.swift"; sourceTree = ""; }; DBCE2ED72591FDF000926D09 /* FollowingListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+State.swift"; sourceTree = ""; }; - DBCE2EE22593319000926D09 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; DBCE2EE725933B1600926D09 /* SearchUserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUserTableViewCell.swift; sourceTree = ""; }; DBD0B49C2758B5F50015A388 /* SidebarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarSection.swift; sourceTree = ""; }; DBD0B49F2758B6010015A388 /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; @@ -851,6 +845,7 @@ DBED96D7253F5D7800C5383A /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DBEFDC8725591F5C0086F268 /* DrawerSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarViewController.swift; sourceTree = ""; }; DBEFDC91255920060086F268 /* DrawerSidebarTransitionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerSidebarTransitionController.swift; sourceTree = ""; }; + DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AvatarBarButtonItemDelegate.swift"; sourceTree = ""; }; DBF3309025B96E0B00A678FB /* WKNavigationDelegateShim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKNavigationDelegateShim.swift; sourceTree = ""; }; DBF3309825B988A500A678FB /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; DBF639E9259B11F6009E12C8 /* UserTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+Diffable.swift"; sourceTree = ""; }; @@ -975,16 +970,6 @@ path = Mastodon; sourceTree = ""; }; - DB01091926E60668005F67D7 /* Deprecated */ = { - isa = PBXGroup; - children = ( - DB43CBDC2595CE2100D47607 /* LoadMoreConfigurableViewContainer.swift */, - DB8AC0FF25401C2700E636BE /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */, - DBCE2EE22593319000926D09 /* AvatarConfigurableView.swift */, - ); - path = Deprecated; - sourceTree = ""; - }; DB02C76E27350D75007EA0BF /* Hashtag */ = { isa = PBXGroup; children = ( @@ -2280,12 +2265,11 @@ DBED96DC253F5D7B00C5383A /* Protocol */ = { isa = PBXGroup; children = ( - DB01091926E60668005F67D7 /* Deprecated */, DB5632A526DCC82D00FC893F /* Provider */, + DBF167FC27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift */, DBED96D7253F5D7800C5383A /* NamingState.swift */, DBC8E04A2576337F00401E20 /* DisposeBagCollectable.swift */, DB36F35D257F74C00028F81E /* ScrollViewContainer.swift */, - DB2FFF60258CB051003DBC19 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */, DB6B8B5E25AF30B900F20FD5 /* ContextMenuAction.swift */, DB51DC3A2716F82000A0D8FB /* CellFrameCacheContainer.swift */, ); @@ -2965,7 +2949,6 @@ DBDAF243274F530B00050319 /* SceneCoordinator.swift in Sources */, DB25C4CC27799FE600EC1435 /* SavedSearchViewController.swift in Sources */, DB830200273D04D000BF5224 /* NotificationTimelineViewModel+Diffable.swift in Sources */, - DB01091A26E60693005F67D7 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */, DBCB4060255CAC0300DD8D8F /* TwitterAuthenticationController.swift in Sources */, DB5632B426DDE3F300FC893F /* StatusThreadViewModel+LoadThreadState.swift in Sources */, DBD0B49D2758B5F50015A388 /* SidebarSection.swift in Sources */, @@ -2979,7 +2962,6 @@ DBA21039275A0E77000B7CB2 /* FollowerListViewController.swift in Sources */, DB676180254C042C006C6798 /* FriendshipTableViewCell.swift in Sources */, DB5632C226DF8DE100FC893F /* TimelineViewModel+LoadOldestState.swift in Sources */, - DBCE2EE32593319000926D09 /* AvatarConfigurableView.swift in Sources */, DB3B906326E8BBD70010F64C /* ProfileHeaderView.swift in Sources */, DB02C77227351B7D007EA0BF /* HashtagTableViewCell.swift in Sources */, DB3B906726E8CD6D0010F64C /* ProfileHeaderViewModel.swift in Sources */, @@ -3011,6 +2993,7 @@ DBC8E04B2576337F00401E20 /* DisposeBagCollectable.swift in Sources */, DB71C7C6271D304900BE3819 /* UserLikeTimelineViewController+DataSourceProvider.swift in Sources */, DBAA898E2758CF01001C273B /* DrawerSidebarHeaderView+ViewModel.swift in Sources */, + DBF167FD27C394830001F75E /* NeedsDependency+AvatarBarButtonItemDelegate.swift in Sources */, DB02C77927352217007EA0BF /* SearchHashtagViewModel+Diffable.swift in Sources */, DB262A4327229BE700D18EF3 /* SearchTimelineViewController.swift in Sources */, DB25C4CA277999BE00EC1435 /* ButtonTableViewCell.swift in Sources */, @@ -3138,7 +3121,6 @@ DB5FAFFD2727B940006520FA /* UserListFetchViewModel.swift in Sources */, DB01092226E608B7005F67D7 /* DataSourceFacade+Repost.swift in Sources */, DB67610D254AD681006C6798 /* ActivityIndicatorCollectionViewCell.swift in Sources */, - DB2FFF61258CB051003DBC19 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */, DB94B6B426C65BE100A2E8A1 /* MastodonAuthenticationController.swift in Sources */, DBA6B1B725355C1E005FB0B4 /* UIAlertController.swift in Sources */, DB1BF29925663EFA00B5F490 /* DisplayPreference.swift in Sources */, @@ -3317,11 +3299,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 74; + DYLIB_CURRENT_VERSION = 77; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3351,11 +3333,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 7LFDZ96332; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 74; + DYLIB_CURRENT_VERSION = 77; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2021 Twidere. All rights reserved."; @@ -3381,7 +3363,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3407,7 +3389,7 @@ buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; @@ -3557,7 +3539,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3565,7 +3547,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3583,7 +3565,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TwidereX/TwidereX.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_ASSET_PATHS = "TwidereX/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereX/Info.plist; @@ -3591,7 +3573,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.0; + MARKETING_VERSION = 1.2.5; PRODUCT_BUNDLE_IDENTIFIER = com.twidere.TwidereX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3606,7 +3588,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3630,7 +3612,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3653,7 +3635,7 @@ baseConfigurationReference = 2E41729E598B3379FC09BC03 /* Pods-TwidereX-TwidereXUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3676,7 +3658,7 @@ baseConfigurationReference = 44D01BA9E21B7F374DBDA38A /* Pods-TwidereX-TwidereXUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 77; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = TwidereXUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved index f0b19029..e333c21b 100644 --- a/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/TwidereX.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,15 +10,6 @@ "version": "5.5.0" } }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage", - "state": { - "branch": null, - "revision": "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version": "4.2.0" - } - }, { "package": "CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog", @@ -33,8 +24,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "4b0565384d3c4c588af09e660535b2c7c9bf5b39", - "version": "1.4.2" + "revision": "12f2389aca4a07e0dd54c86ec23d0721ed88b8db", + "version": "1.4.3" } }, { @@ -51,7 +42,7 @@ "repositoryURL": "https://github.com/kciter/Floaty.git", "state": { "branch": "master", - "revision": "1a6fd17ae1469c86baf830d9d5c93e6804cabe95", + "revision": "f8f6cf9970a3d92e98fcc2cf123b1bdef15e8778", "version": null } }, @@ -105,8 +96,8 @@ "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "state": { "branch": null, - "revision": "86f6be38c2bc322fcdbec24a2377f8e67566f7da", - "version": "3.2.1" + "revision": "95a092656315e38b433271a0470105be0025bd67", + "version": "3.3.1" } }, { @@ -145,22 +136,13 @@ "version": "2.36.0" } }, - { - "package": "SwiftMessages", - "repositoryURL": "https://github.com/SwiftKickMobile/SwiftMessages", - "state": { - "branch": null, - "revision": "b29dd21090b708aa0ae9ecbaf6e2d0487028dc3f", - "version": "9.0.6" - } - }, { "package": "Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", "state": { "branch": null, - "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2", - "version": "0.1.3" + "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", + "version": "0.1.4" } }, { @@ -177,8 +159,8 @@ "repositoryURL": "https://github.com/uias/Tabman.git", "state": { "branch": null, - "revision": "f43489cdd743ba7ad86a422ebb5fcbf34e333df4", - "version": "2.11.1" + "revision": "a9f10cb862a32e6a22549836af013abd6b0692d3", + "version": "2.12.0" } }, { diff --git a/TwidereX/Coordinator/SceneCoordinator.swift b/TwidereX/Coordinator/SceneCoordinator.swift index abcb523f..059506c3 100644 --- a/TwidereX/Coordinator/SceneCoordinator.swift +++ b/TwidereX/Coordinator/SceneCoordinator.swift @@ -74,16 +74,9 @@ extension SceneCoordinator { case trend(viewModel: TrendViewModel) case searchResult(viewModel: SearchResultViewModel) - // TODO: - // case tweetConversation(viewModel: TweetConversationViewModel) -// case searchDetail(viewModel: SearchDetailViewModel) - -// case friendshipList(viewModel: FriendshipListViewModel) - case setting case displayPreference case about - // end TODO: #if DEBUG case developer diff --git a/TwidereX/Diffable/Msic/Notification/NotificationSection.swift b/TwidereX/Diffable/Msic/Notification/NotificationSection.swift index 08f1f5a7..e9163b31 100644 --- a/TwidereX/Diffable/Msic/Notification/NotificationSection.swift +++ b/TwidereX/Diffable/Msic/Notification/NotificationSection.swift @@ -9,6 +9,7 @@ import UIKit import AppShared +import TwidereUI enum NotificationSection: Hashable { case main @@ -17,8 +18,9 @@ enum NotificationSection: Hashable { extension NotificationSection { struct Configuration { - let statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate - let userTableViewCellDelegate: UserTableViewCellDelegate? + weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? + weak var userTableViewCellDelegate: UserTableViewCellDelegate? + let statusViewConfigurationContext: StatusView.ConfigurationContext } static func diffableDataSource( @@ -29,9 +31,7 @@ extension NotificationSection { return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in // data source should dispatch in main thread assert(Thread.isMainThread) - - let activeAuthenticationContext = context.authenticationService.activeAuthenticationContext.eraseToAnyPublisher() - + // configure cell with item switch item { case .feed(let record): @@ -46,20 +46,13 @@ extension NotificationSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell StatusSection.setupStatusPollDataSource( context: context, - managedObjectContext: context.managedObjectContext, statusView: cell.statusView, - configurationContext: PollOptionView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - activeAuthenticationContext: activeAuthenticationContext - ) + configurationContext: configuration.statusViewConfigurationContext ) configure( tableView: tableView, cell: cell, - viewModel: StatusTableViewCell.ViewModel( - value: .feed(feed), - activeAuthenticationContext: context.authenticationService.activeAuthenticationContext.share().eraseToAnyPublisher() - ), + viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), configuration: configuration ) return cell @@ -67,7 +60,7 @@ extension NotificationSection { switch object { case .mastodon(let notification): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserNotificationStyleTableViewCell.self), for: indexPath) as! UserNotificationStyleTableViewCell - let authenticationContext = context.authenticationService.activeAuthenticationContext.value + let authenticationContext = context.authenticationService.activeAuthenticationContext let me = authenticationContext?.user(in: context.managedObjectContext) let user: UserObject = .mastodon(object: notification.account) configure( @@ -114,6 +107,7 @@ extension NotificationSection { cell.configure( tableView: tableView, viewModel: viewModel, + configurationContext: configuration.statusViewConfigurationContext, delegate: configuration.statusViewTableViewCellDelegate ) } diff --git a/TwidereX/Diffable/Status/StatusSection.swift b/TwidereX/Diffable/Status/StatusSection.swift index ad5188f7..2d95d557 100644 --- a/TwidereX/Diffable/Status/StatusSection.swift +++ b/TwidereX/Diffable/Status/StatusSection.swift @@ -26,6 +26,7 @@ extension StatusSection { struct Configuration { weak var statusViewTableViewCellDelegate: StatusViewTableViewCellDelegate? weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate? + let statusViewConfigurationContext: StatusView.ConfigurationContext } static func diffableDataSource( @@ -41,30 +42,21 @@ extension StatusSection { // data source should dispatch in main thread assert(Thread.isMainThread) - let activeAuthenticationContext = context.authenticationService.activeAuthenticationContext.eraseToAnyPublisher() - // configure cell with item switch item { case .feed(let record): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell setupStatusPollDataSource( context: context, - managedObjectContext: context.managedObjectContext, statusView: cell.statusView, - configurationContext: PollOptionView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - activeAuthenticationContext: activeAuthenticationContext - ) + configurationContext: configuration.statusViewConfigurationContext ) context.managedObjectContext.performAndWait { guard let feed = record.object(in: context.managedObjectContext) else { return } configure( tableView: tableView, cell: cell, - viewModel: StatusTableViewCell.ViewModel( - value: .feed(feed), - activeAuthenticationContext: activeAuthenticationContext - ), + viewModel: StatusTableViewCell.ViewModel(value: .feed(feed)), configuration: configuration ) } @@ -85,12 +77,8 @@ extension StatusSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell setupStatusPollDataSource( context: context, - managedObjectContext: context.managedObjectContext, statusView: cell.statusView, - configurationContext: PollOptionView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - activeAuthenticationContext: activeAuthenticationContext - ) + configurationContext: configuration.statusViewConfigurationContext ) context.managedObjectContext.performAndWait { switch status { @@ -99,10 +87,7 @@ extension StatusSection { configure( tableView: tableView, cell: cell, - viewModel: StatusTableViewCell.ViewModel( - value: .twitterStatus(status), - activeAuthenticationContext: activeAuthenticationContext - ), + viewModel: StatusTableViewCell.ViewModel(value: .twitterStatus(status)), configuration: configuration ) case .mastodon(let record): @@ -110,10 +95,7 @@ extension StatusSection { configure( tableView: tableView, cell: cell, - viewModel: StatusTableViewCell.ViewModel( - value: .mastodonStatus(status), - activeAuthenticationContext: activeAuthenticationContext - ), + viewModel: StatusTableViewCell.ViewModel(value: .mastodonStatus(status)), configuration: configuration ) } // end switch @@ -127,8 +109,6 @@ extension StatusSection { indexPath: indexPath, configuration: ThreadCellRegistrationConfiguration( thread: thread, - managedObjectContext: context.managedObjectContext, - activeAuthenticationContext: activeAuthenticationContext, configuration: configuration ) ) @@ -151,8 +131,6 @@ extension StatusSection { struct ThreadCellRegistrationConfiguration { let thread: StatusItem.Thread - let managedObjectContext: NSManagedObjectContext - let activeAuthenticationContext: AnyPublisher let configuration: Configuration } @@ -162,26 +140,24 @@ extension StatusSection { indexPath: IndexPath, configuration: ThreadCellRegistrationConfiguration ) -> UITableViewCell { + let managedObjectContext = context.managedObjectContext + + let configurationContext = configuration.configuration.statusViewConfigurationContext + switch configuration.thread { case .root(let threadContext): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusThreadRootTableViewCell.self), for: indexPath) as! StatusThreadRootTableViewCell setupStatusPollDataSource( context: context, - managedObjectContext: configuration.managedObjectContext, statusView: cell.statusView, - configurationContext: PollOptionView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - activeAuthenticationContext: configuration.activeAuthenticationContext - ) + configurationContext: configurationContext ) - configuration.managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: configuration.managedObjectContext) else { return } + managedObjectContext.performAndWait { + guard let status = threadContext.status.object(in: managedObjectContext) else { return } cell.configure( tableView: tableView, - viewModel: StatusThreadRootTableViewCell.ViewModel( - value: .statusObject(status), - activeAuthenticationContext: configuration.activeAuthenticationContext - ), + viewModel: StatusThreadRootTableViewCell.ViewModel(value: .statusObject(status)), + configurationContext: configurationContext, delegate: configuration.configuration.statusViewTableViewCellDelegate ) } @@ -194,21 +170,15 @@ extension StatusSection { let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell setupStatusPollDataSource( context: context, - managedObjectContext: configuration.managedObjectContext, statusView: cell.statusView, - configurationContext: PollOptionView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - activeAuthenticationContext: configuration.activeAuthenticationContext - ) + configurationContext: configurationContext ) - configuration.managedObjectContext.performAndWait { - guard let status = threadContext.status.object(in: configuration.managedObjectContext) else { return } + managedObjectContext.performAndWait { + guard let status = threadContext.status.object(in: managedObjectContext) else { return } cell.configure( tableView: tableView, - viewModel: StatusTableViewCell.ViewModel( - value: .statusObject(status), - activeAuthenticationContext: configuration.activeAuthenticationContext - ), + viewModel: StatusTableViewCell.ViewModel(value: .statusObject(status)), + configurationContext: configurationContext, delegate: configuration.configuration.statusViewTableViewCellDelegate ) } @@ -224,10 +194,10 @@ extension StatusSection { public static func setupStatusPollDataSource( context: AppContext, - managedObjectContext: NSManagedObjectContext, statusView: StatusView, configurationContext: PollOptionView.ConfigurationContext ) { + let managedObjectContext = context.managedObjectContext statusView.pollTableViewDiffableDataSource = UITableViewDiffableDataSource(tableView: statusView.pollTableView) { tableView, indexPath, item in switch item { case .option(let record): @@ -276,7 +246,7 @@ extension StatusSection { }() if needsUpdatePoll, - case let .mastodon(authenticationContext) = context.authenticationService.activeAuthenticationContext.value + case let .mastodon(authenticationContext) = context.authenticationService.activeAuthenticationContext { let status: ManagedObjectRecord = .init(objectID: option.poll.status.objectID) Task { [weak context] in @@ -309,6 +279,7 @@ extension StatusSection { cell.configure( tableView: tableView, viewModel: viewModel, + configurationContext: configuration.statusViewConfigurationContext, delegate: configuration.statusViewTableViewCellDelegate ) } diff --git a/TwidereX/Diffable/User/UserSection.swift b/TwidereX/Diffable/User/UserSection.swift index 8e8b7d50..e8e51b51 100644 --- a/TwidereX/Diffable/User/UserSection.swift +++ b/TwidereX/Diffable/User/UserSection.swift @@ -66,7 +66,7 @@ extension UserSection { let cell = dequeueReusableCell(tableView: tableView, indexPath: indexPath, style: style) context.managedObjectContext.performAndWait { guard let user = record.object(in: context.managedObjectContext) else { return } - let authenticationContext = context.authenticationService.activeAuthenticationContext.value + let authenticationContext = context.authenticationService.activeAuthenticationContext let me = authenticationContext?.user(in: context.managedObjectContext) let viewModel = UserTableViewCell.ViewModel( user: user, diff --git a/TwidereX/Info.plist b/TwidereX/Info.plist index 0f043fce..ace1f2c9 100644 --- a/TwidereX/Info.plist +++ b/TwidereX/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.4 + 1.2.6 CFBundleVersion - 74 + 77 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/TwidereX/Protocol/Deprecated/AvatarConfigurableView.swift b/TwidereX/Protocol/Deprecated/AvatarConfigurableView.swift deleted file mode 100644 index d147494a..00000000 --- a/TwidereX/Protocol/Deprecated/AvatarConfigurableView.swift +++ /dev/null @@ -1,217 +0,0 @@ -// -// AvatarConfigurableView.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-12-23. -// Copyright © 2020 Twidere. All rights reserved. -// - -import UIKit -import AlamofireImage -import Kingfisher - -//@available(*, deprecated, message: "") -//protocol AvatarConfigurableView { -// static var configurableAvatarImageViewSize: CGSize { get } -// static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { get } -// var configurableAvatarImageView: UIImageView? { get } -// var configurableAvatarButton: UIButton? { get } -// var configurableVerifiedBadgeImageView: UIImageView? { get } -// func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) -// func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) -//} -// -//@available(*, deprecated, message: "") -//extension AvatarConfigurableView { -// -// static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { return .mini } -// -// public func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) { -// // set badge -// switch (input.verified, input.blocked) { -// case (_, true): -// configurableVerifiedBadgeImageView?.isHidden = false -// switch Self.configurableAvatarImageViewBadgeAppearanceStyle { -// case .mini: -// assertionFailure() -// configurableVerifiedBadgeImageView?.image = Asset.ObjectTools.blockedBadge.image.withRenderingMode(.alwaysOriginal) -// case .normal: -// configurableVerifiedBadgeImageView?.image = Asset.ObjectTools.blockedBadge.image.withRenderingMode(.alwaysOriginal) -// } -// case (true, false): -// configurableVerifiedBadgeImageView?.isHidden = false -// switch Self.configurableAvatarImageViewBadgeAppearanceStyle { -// case .mini: -// break -//// configurableVerifiedBadgeImageView?.image = Asset.ObjectTools.verifiedBadgeMini.image.withRenderingMode(.alwaysOriginal) -// case .normal: -// break -//// configurableVerifiedBadgeImageView?.image = Asset.ObjectTools.verifiedBadge.image.withRenderingMode(.alwaysOriginal) -// } -// default: -// configurableVerifiedBadgeImageView?.isHidden = true -// } -// -// let avatarStyle = UserDefaults.shared.avatarStyle -// let roundedSquareCornerRadius = AvatarConfigurableViewConfiguration.roundedSquareCornerRadius(for: Self.configurableAvatarImageViewSize) -// let cornerRadius = AvatarConfigurableViewConfiguration.cornerRadius(for: Self.configurableAvatarImageViewSize, avatarStyle: avatarStyle) -// let scale = (configurableAvatarImageView ?? configurableAvatarButton)?.window?.screen.scale ?? UIScreen.main.scale -// -// let placeholderImage: UIImage = { -// let placeholderImage = input.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageViewSize, color: .systemFill) -// switch avatarStyle { -// case .circle: return placeholderImage.af.imageRoundedIntoCircle() -// case .roundedSquare: return placeholderImage.af.imageRounded(withCornerRadius: roundedSquareCornerRadius * placeholderImage.scale, divideRadiusByImageScale: true) -// } -// -// }() -// -// // cancel previous task -// configurableAvatarImageView?.af.cancelImageRequest() -// configurableAvatarImageView?.kf.cancelDownloadTask() -// configurableAvatarButton?.af.cancelImageRequest(for: .normal) -// configurableAvatarButton?.kf.cancelImageDownloadTask() -// -// // reset layer attributes -// configurableAvatarImageView?.layer.masksToBounds = false -// configurableAvatarImageView?.layer.cornerRadius = 0 -// configurableAvatarImageView?.layer.cornerCurve = .circular -// -// configurableAvatarButton?.layer.masksToBounds = false -// configurableAvatarButton?.layer.cornerRadius = 0 -// configurableAvatarButton?.layer.cornerCurve = .circular -// -// defer { -// let configuration = AvatarConfigurableViewConfiguration( -// input: input, -// output: AvatarConfigurableViewConfiguration.Output(cornerRadius: cornerRadius) -// ) -// avatarConfigurableView(self, didFinishConfiguration: configuration) -// } -// -// // set placeholder if no asset -// guard let avatarImageURL = input.avatarImageURL else { -// configurableAvatarImageView?.image = placeholderImage -// configurableAvatarButton?.setImage(placeholderImage, for: .normal) -// return -// } -// -// if let avatarImageView = configurableAvatarImageView { -// // set avatar (GIF using Kingfisher) -// switch avatarImageURL.pathExtension { -// case "gif": -// avatarImageView.kf.setImage( -// with: avatarImageURL, -// placeholder: placeholderImage, -// options: [ -// .transition(.fade(0.2)) -// ] -// ) -// avatarImageView.layer.masksToBounds = true -// avatarImageView.layer.cornerRadius = cornerRadius -// switch avatarStyle { -// case .circle: avatarImageView.layer.cornerCurve = .circular -// case .roundedSquare: avatarImageView.layer.cornerCurve = .continuous -// } -// default: -// let filter = avatarImageFilter(for: avatarStyle, roundedSquareCornerRadius: roundedSquareCornerRadius, scale: scale) -// avatarImageView.af.setImage( -// withURL: avatarImageURL, -// placeholderImage: placeholderImage, -// filter: filter, -// imageTransition: .crossDissolve(0.3), -// runImageTransitionIfCached: false, -// completion: nil -// ) -// } -// } -// -// if let avatarButton = configurableAvatarButton { -// switch avatarImageURL.pathExtension { -// case "gif": -// avatarButton.kf.setImage( -// with: avatarImageURL, -// for: .normal, -// placeholder: placeholderImage, -// options: [ -// .transition(.fade(0.2)) -// ] -// ) -// avatarButton.layer.masksToBounds = true -// avatarButton.layer.cornerRadius = cornerRadius -// switch avatarStyle { -// case .circle: avatarButton.layer.cornerCurve = .circular -// case .roundedSquare: avatarButton.layer.cornerCurve = .continuous -// } -// default: -// let filter = avatarImageFilter(for: avatarStyle, roundedSquareCornerRadius: roundedSquareCornerRadius, scale: scale) -// avatarButton.af.setImage( -// for: .normal, -// url: avatarImageURL, -// placeholderImage: placeholderImage, -// filter: filter, -// completion: nil -// ) -// } -// } -// } -// -// func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) { } -// -//} -// -//extension AvatarConfigurableView { -// -// private func avatarImageFilter(for avatarStyle: UserDefaults.AvatarStyle, roundedSquareCornerRadius radius: CGFloat, scale: CGFloat) -> ImageFilter { -// switch avatarStyle { -// case .circle: return ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize) -// case .roundedSquare: return AspectScaledToFillSizeWithRoundedCornersFilter(size: Self.configurableAvatarImageViewSize, radius: radius * scale, divideRadiusByImageScale: true) -// } -// } -// -//} -// -//struct AvatarConfigurableViewConfiguration { -// -// enum BadgeAppearanceStyle { -// case mini -// case normal -// } -// -// struct Input { -// let avatarImageURL: URL? -// let placeholderImage: UIImage? -// let blocked: Bool -// let verified: Bool -// -// init(avatarImageURL: URL?, placeholderImage: UIImage? = nil, blocked: Bool = false, verified: Bool = false) { -// self.avatarImageURL = avatarImageURL -// self.placeholderImage = placeholderImage -// self.blocked = blocked -// self.verified = verified -// } -// } -// -// struct Output { -// let cornerRadius: CGFloat -// } -// -// let input: Input -// let output: Output -// -// static func roundedSquareCornerRadius(for imageSize: CGSize) -> CGFloat { -// return CGFloat(Int(imageSize.width) / 8 * 2) // even number from quoter of width -// } -// -// static func cornerRadius(for imageSize: CGSize, avatarStyle: UserDefaults.AvatarStyle) -> CGFloat { -// let roundedSquareCornerRadius = Self.roundedSquareCornerRadius(for: imageSize) -// let cornerRadius: CGFloat = { -// switch avatarStyle { -// case .circle: return 0.5 * imageSize.width -// case .roundedSquare: return roundedSquareCornerRadius -// } -// }() -// return cornerRadius -// } -// -//} diff --git a/TwidereX/Protocol/Deprecated/ContentOffsetAdjustableTimelineViewControllerDelegate.swift b/TwidereX/Protocol/Deprecated/ContentOffsetAdjustableTimelineViewControllerDelegate.swift deleted file mode 100644 index 142f7df0..00000000 --- a/TwidereX/Protocol/Deprecated/ContentOffsetAdjustableTimelineViewControllerDelegate.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ContentOffsetAdjustableTimelineViewControllerDelegate.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-10-21. -// Copyright © 2020 Twidere. All rights reserved. -// - -import UIKit - -@available(*, deprecated, message: "") -protocol ContentOffsetAdjustableTimelineViewControllerDelegate: class { - func navigationBar() -> UINavigationBar? -} - diff --git a/TwidereX/Protocol/Deprecated/LoadMoreConfigurableViewContainer.swift b/TwidereX/Protocol/Deprecated/LoadMoreConfigurableViewContainer.swift deleted file mode 100644 index 868fd85c..00000000 --- a/TwidereX/Protocol/Deprecated/LoadMoreConfigurableViewContainer.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// LoadMoreConfigurableViewContainer.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-12-25. -// Copyright © 2020 Twidere. All rights reserved. -// - -import UIKit -import GameplayKit - -/// The tableView container driven by state machines with "LoadMore" logic -//protocol LoadMoreConfigurableTableViewContainer: UIViewController { -// -// associatedtype BottomLoaderTableViewCell: UITableViewCell -// associatedtype LoadingState: GKState -// -// var loadMoreConfigurableTableView: UITableView { get } -// var loadMoreConfigurableStateMachine: GKStateMachine { get } -// func handleScrollViewDidScroll(_ scrollView: UIScrollView) -//} -// -//extension LoadMoreConfigurableTableViewContainer { -// func handleScrollViewDidScroll(_ scrollView: UIScrollView) { -// guard scrollView === loadMoreConfigurableTableView else { return } -// -// // check if current scroll position is the bottom of table -// let contentOffsetY = loadMoreConfigurableTableView.contentOffset.y -// let bottomVisiblePageContentOffsetY = loadMoreConfigurableTableView.contentSize.height - (1.5 * loadMoreConfigurableTableView.visibleSize.height) -// guard contentOffsetY > bottomVisiblePageContentOffsetY else { -// return -// } -// -// let cells = loadMoreConfigurableTableView.visibleCells.compactMap { $0 as? BottomLoaderTableViewCell } -// guard let loaderTableViewCell = cells.first else { return } -// -// if let tabBar = tabBarController?.tabBar, let window = view.window { -// let loaderTableViewCellFrameInWindow = loadMoreConfigurableTableView.convert(loaderTableViewCell.frame, to: nil) -// let windowHeight = window.frame.height -// let loaderAppear = (loaderTableViewCellFrameInWindow.origin.y + 0.8 * loaderTableViewCell.frame.height) < (windowHeight - tabBar.frame.height) -// if loaderAppear { -// loadMoreConfigurableStateMachine.enter(LoadingState.self) -// } else { -// // do nothing -// } -// } else { -// loadMoreConfigurableStateMachine.enter(LoadingState.self) -// } -// } -//} diff --git a/TwidereX/Protocol/NeedsDependency+AVPlayerViewControllerDelegate.swift b/TwidereX/Protocol/NeedsDependency+AVPlayerViewControllerDelegate.swift deleted file mode 100644 index 7cc789dc..00000000 --- a/TwidereX/Protocol/NeedsDependency+AVPlayerViewControllerDelegate.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// NeedsDependency+AVPlayerViewControllerDelegate.swift -// TwidereX -// -// Created by Cirno MainasuK on 2020-12-18. -// Copyright © 2020 Twidere. All rights reserved. -// - -import Foundation -import AVKit - -extension NeedsDependency where Self: AVPlayerViewControllerDelegate { - - func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = true - } - - func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = false - } - -} diff --git a/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift b/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift new file mode 100644 index 00000000..0a3b4f14 --- /dev/null +++ b/TwidereX/Protocol/NeedsDependency+AvatarBarButtonItemDelegate.swift @@ -0,0 +1,31 @@ +// +// NeedsDependency+AvatarBarButtonItemDelegate.swift +// TwidereX +// +// Created by MainasuK on 2022-2-21. +// Copyright © 2022 Twidere. All rights reserved. +// + +import UIKit + +// MARK: - AvatarBarButtonItemDelegate +extension NeedsDependency where Self: AvatarBarButtonItemDelegate { + + func avatarBarButtonItem( + _ barButtonItem: AvatarBarButtonItem, + didLongPressed sender: UILongPressGestureRecognizer + ) { + Task { @MainActor in + let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) + feedbackGenerator.impactOccurred() + + let accountListViewModel = AccountListViewModel(context: context) + coordinator.present( + scene: .accountList(viewModel: accountListViewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) + } // end Task + } + +} diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift index e70226c7..2b2dfde2 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Poll.swift @@ -127,7 +127,7 @@ extension DataSourceFacade { status: ManagedObjectRecord, voteButtonDidPressed button: UIButton ) async throws { - guard case let .mastodon(authenticationContext) = provider.context.authenticationService.activeAuthenticationContext.value else { return } + guard case let .mastodon(authenticationContext) = provider.context.authenticationService.activeAuthenticationContext else { return } // should use same context on UI to make transient property trigger update let managedObjectContext = provider.context.managedObjectContext diff --git a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift index 921e3e4e..1bef6f2c 100644 --- a/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift +++ b/TwidereX/Protocol/Provider/DataSourceFacade+Status.swift @@ -36,8 +36,11 @@ extension DataSourceFacade { apiService: provider.context.apiService, authenticationService: provider.context.authenticationService, mastodonEmojiService: provider.context.mastodonEmojiService, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: provider.context.authenticationService.$activeAuthenticationContext + ) ) ) provider.coordinator.present( @@ -103,13 +106,14 @@ extension DataSourceFacade { provider: DataSourceProvider, status: StatusRecord ) async throws { - try await provider.context.backgroundManagedObjectContext.performChanges { - guard let object = status.object(in: provider.context.managedObjectContext) else { return } + let managedObjectContext = provider.context.managedObjectContext + try await managedObjectContext.performChanges { + guard let object = status.object(in: managedObjectContext) else { return } switch object { case .twitter: break case .mastodon(let status): - status.update(isContentReveal: !status.isContentReveal) + status.update(isContentSensitiveToggled: !status.isContentSensitiveToggled) } } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift index f3198662..32c2d86b 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+MediaInfoDescriptionViewDelegate.swift @@ -74,7 +74,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, actionDidPressed action: StatusToolbar.Action, button: UIButton) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { let source = DataSourceItem.Source(tableViewCell: nil, indexPath: nil) guard let item = await item(from: source) else { @@ -97,7 +97,7 @@ extension MediaInfoDescriptionViewDelegate where Self: DataSourceProvider { } // end func func mediaInfoDescriptionView(_ mediaInfoDescriptionView: MediaInfoDescriptionView, statusToolbar: StatusToolbar, menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } } diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift index f01e39f5..8d56fd43 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+StatusViewTableViewCellDelegate.swift @@ -321,7 +321,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { actionDidPressed action: StatusToolbar.Action, button: UIButton ) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { @@ -350,7 +350,7 @@ extension StatusViewTableViewCellDelegate where Self: DataSourceProvider { menuActionDidPressed action: StatusToolbar.MenuAction, menuButton button: UIButton ) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) guard let item = await item(from: source) else { diff --git a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index c40232eb..681519d3 100644 --- a/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/TwidereX/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -120,17 +120,27 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewTrans return nil } - func aspectTableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + func aspectTableView( + _ tableView: UITableView, + previewForHighlightingContextMenuWithConfiguration + configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") return aspectTableView(tableView, configuration: configuration) } - func aspectTableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + func aspectTableView( + _ tableView: UITableView, + previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") return aspectTableView(tableView, configuration: configuration) } - private func aspectTableView(_ tableView: UITableView, configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + private func aspectTableView( + _ tableView: UITableView, + configuration: UIContextMenuConfiguration + ) -> UITargetedPreview? { guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return nil } guard let indexPath = configuration.indexPath, let index = configuration.index else { return nil } if let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell { @@ -146,7 +156,11 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewTrans } } - func aspectTableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { + func aspectTableView( + _ tableView: UITableView, + willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, + animator: UIContextMenuInteractionCommitAnimating + ) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return } guard let indexPath = configuration.indexPath, let index = configuration.index else { return } @@ -179,6 +193,6 @@ extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewTrans ) } } - } + } // end func } diff --git a/TwidereX/Scene/Account/List/AccountListViewController.swift b/TwidereX/Scene/Account/List/AccountListViewController.swift index 5e45b87d..ced42280 100644 --- a/TwidereX/Scene/Account/List/AccountListViewController.swift +++ b/TwidereX/Scene/Account/List/AccountListViewController.swift @@ -14,10 +14,6 @@ import CoreDataStack import TwitterSDK import TwidereCommon -protocol AccountListViewControllerDelegate: AnyObject { - func signoutTwitterUser(id: TwitterUser.ID) -} - final class AccountListViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } @@ -43,7 +39,7 @@ final class AccountListViewController: UIViewController, NeedsDependency { }() deinit { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } } @@ -131,65 +127,5 @@ extension AccountListViewController: UITableViewDelegate { } } -// MARK: - AccountListTableViewCellDelegate -extension AccountListViewController: AccountListTableViewCellDelegate { - - func accountListTableViewCell(_ cell: AccountListTableViewCell, menuButtonPressed button: UIButton) { -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let indexPath = tableView.indexPath(for: cell) else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// guard case let .twitterUser(objectID) = item else { return } -// -// let managedObjectContext = context.managedObjectContext -// managedObjectContext.perform { -// guard let twitterUser = managedObjectContext.object(with: objectID) as? TwitterUser else { return } -// let title = twitterUser.name -// -// DispatchQueue.main.async { [weak self] in -// let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) -// let signOutAction = UIAlertAction(title: L10n.Scene.ManageAccounts.deleteAccount.localizedCapitalized, style: .destructive) { [weak self] _ in -// guard let self = self else { return } -// self.signoutTwitterUser(id: twitterUser.id) -// } -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) -// alertController.addAction(signOutAction) -// alertController.addAction(cancelAction) -// guard let self = self else { return } -// alertController.popoverPresentationController?.sourceView = button -// self.present(alertController, animated: true, completion: nil) -// } -// } - } - -} - -// MARK: - AccountListViewControllerDelegate -extension AccountListViewController: AccountListViewControllerDelegate { - - func signoutTwitterUser(id: TwitterUser.ID) { -// let currentAccountCount = viewModel.diffableDataSource.snapshot().itemIdentifiers.count -// context.authenticationService.signOutTwitterUser(id: id) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] result in -// guard let self = self else { return } -// switch result { -// case .failure(let error): -// assertionFailure(error.localizedDescription) -// case .success(let isSignOut): -// guard isSignOut else { return } -// self.dismiss(animated: true) { -// if currentAccountCount == 1 { -// // No active user. Present Authentication scene -// let authenticationViewModel = AuthenticationViewModel(isAuthenticationIndexExist: false) -// self.coordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: true, completion: nil)) -// } -// } -// } -// } -// .store(in: &disposeBag) - } - -} - // MARK: - UserTableViewCellDelegate extension AccountListViewController: UserTableViewCellDelegate { } diff --git a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift index 47c3690d..03a98992 100644 --- a/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift +++ b/TwidereX/Scene/Account/List/AccountListViewModel+Diffable.swift @@ -47,57 +47,4 @@ extension AccountListViewModel { .store(in: &disposeBag) } -// static func configure(cell: AccountListTableViewCell, twitterUser: TwitterUser, accountListViewControllerDelegate: AccountListViewControllerDelegate?) { -// // set avatar -// let avatarImageURL = twitterUser.avatarImageURL() -// let verified = twitterUser.verified -// UserDefaults.shared -// .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in -// cell.userBriefInfoView.configure(withConfigurationInput: AvatarConfigurableViewConfiguration.Input(avatarImageURL: avatarImageURL, verified: verified)) -// } -// .store(in: &cell.observations) -// -// cell.userBriefInfoView.lockImageView.isHidden = !twitterUser.protected -// -// // set name and username -// cell.userBriefInfoView.nameLabel.text = twitterUser.name -// cell.userBriefInfoView.headerSecondaryLabel.text = "" -// -// cell.userBriefInfoView.detailLabel.text = "@" + twitterUser.username -// -// if let accountListViewControllerDelegate = accountListViewControllerDelegate { -// if #available(iOS 14.0, *) { -// let menuItems = [ -// UIMenu( -// title: L10n.Scene.ManageAccounts.deleteAccount, -// options: .destructive, -// children: [ -// UIAction( -// title: L10n.Common.Controls.Actions.remove, -// image: nil, -// attributes: .destructive, -// state: .off, -// handler: { [weak accountListViewControllerDelegate] _ in -// accountListViewControllerDelegate?.signoutTwitterUser(id: twitterUser.id) -// } -// ), -// UIAction( -// title: L10n.Common.Controls.Actions.cancel, -// attributes: [], -// state: .off, -// handler: { _ in -// // do nothing -// } -// ) -// ] -// ) -// ] -// cell.userBriefInfoView.menuButton.menu = UIMenu(title: "", children: menuItems) -// cell.userBriefInfoView.menuButton.showsMenuAsPrimaryAction = true -// } else { -// // delegate handle the button -// } -// } -// } - } diff --git a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift index 3abd7ed8..fbf39be2 100644 --- a/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift +++ b/TwidereX/Scene/Account/List/View/AccountListTableViewCell.swift @@ -9,21 +9,16 @@ import os.log import UIKit import Combine - -protocol AccountListTableViewCellDelegate: AnyObject { - func accountListTableViewCell(_ cell: AccountListTableViewCell, menuButtonPressed button: UIButton) -} +import TwidereUI final class AccountListTableViewCell: UITableViewCell { var disposeBag = Set() var observations = Set() - weak var delegate: AccountListTableViewCellDelegate? - let userBriefInfoView = UserBriefInfoView() - let separatorLine = UIView.separatorLine + let separatorLine = SeparatorLineView() override func prepareForReuse() { super.prepareForReuse() @@ -72,11 +67,3 @@ extension AccountListTableViewCell { } } - -extension AccountListTableViewCell { - @objc private func menuButtonPressed(_ sender: UIButton) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - delegate?.accountListTableViewCell(self, menuButtonPressed: sender) - } -} diff --git a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewController.swift index ed2c4505..c17c46c0 100644 --- a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -130,8 +130,11 @@ extension HashtagTimelineViewController { apiService: context.apiService, authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) ) coordinator.present(scene: .compose(viewModel: composeViewModel, contentViewModel: composeContentViewModel), from: self, transition: .modal(animated: true, completion: nil)) diff --git a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index 0f0dc76f..5a53dc95 100644 --- a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -7,6 +7,7 @@ // import UIKit +import AppShared extension HashtagTimelineViewModel { func setupDiffableDataSource( @@ -15,7 +16,12 @@ extension HashtagTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 7b452750..375cbd06 100644 --- a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -71,7 +71,7 @@ extension HashtagTimelineViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index 4295756a..f5d40fd6 100644 --- a/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/TwidereX/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -43,7 +43,7 @@ final class HashtagTimelineViewModel { self.statusRecordFetchedResultController = StatusRecordFetchedResultController(managedObjectContext: context.managedObjectContext) // end init - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .map { $0?.userIdentifier } .assign(to: &statusRecordFetchedResultController.$userIdentifier) } diff --git a/TwidereX/Scene/MainTab/MainTabBarController.swift b/TwidereX/Scene/MainTab/MainTabBarController.swift index ef91ac75..a745dd0e 100644 --- a/TwidereX/Scene/MainTab/MainTabBarController.swift +++ b/TwidereX/Scene/MainTab/MainTabBarController.swift @@ -43,14 +43,19 @@ class MainTabBarController: UITabBarController { var image: UIImage { switch self { - case .home: - return Asset.ObjectTools.house.image.withRenderingMode(.alwaysTemplate) - case .notification: - return Asset.ObjectTools.bell.image.withRenderingMode(.alwaysTemplate) - case .search: - return Asset.ObjectTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate) - case .me: - return Asset.Human.person.image.withRenderingMode(.alwaysTemplate) + case .home: return Asset.ObjectTools.house.image.withRenderingMode(.alwaysTemplate) + case .notification: return Asset.ObjectTools.bell.image.withRenderingMode(.alwaysTemplate) + case .search: return Asset.ObjectTools.magnifyingglass.image.withRenderingMode(.alwaysTemplate) + case .me: return Asset.Human.person.image.withRenderingMode(.alwaysTemplate) + } + } + + var largeImage: UIImage { + switch self { + case .home: return Asset.ObjectTools.houseLarge.image.withRenderingMode(.alwaysTemplate) + case .notification: return Asset.ObjectTools.bellLarge.image.withRenderingMode(.alwaysTemplate) + case .search: return Asset.ObjectTools.magnifyingglassLarge.image.withRenderingMode(.alwaysTemplate) + case .me: return Asset.Human.personLarge.image.withRenderingMode(.alwaysTemplate) } } @@ -110,18 +115,16 @@ extension MainTabBarController { let tabs = Tab.allCases let viewControllers: [UIViewController] = tabs.map { tab in let viewController = tab.viewController(context: context, coordinator: coordinator) - viewController.tabBarItem.title = "" // set text to empty string for image only style (SDK failed to layout when set to nil) + viewController.tabBarItem.title = tab.title viewController.tabBarItem.image = tab.image + viewController.tabBarItem.accessibilityLabel = tab.title + viewController.tabBarItem.largeContentSizeImage = tab.largeImage + return viewController } setViewControllers(viewControllers, animated: false) selectedIndex = 0 -// // TODO: custom accent color -// let tabBarAppearance = UITabBarAppearance() -// tabBarAppearance.configureWithDefaultBackground() -// tabBar.standardAppearance = tabBarAppearance - let feedbackGenerator = UINotificationFeedbackGenerator() context.publisherService.statusPublishResult diff --git a/TwidereX/Scene/MediaPreview/Image/MediaPreviewImageView.swift b/TwidereX/Scene/MediaPreview/Image/MediaPreviewImageView.swift index d1c47c56..831bdb0a 100644 --- a/TwidereX/Scene/MediaPreview/Image/MediaPreviewImageView.swift +++ b/TwidereX/Scene/MediaPreview/Image/MediaPreviewImageView.swift @@ -17,6 +17,8 @@ final class MediaPreviewImageView: UIScrollView { imageView.contentMode = .scaleAspectFit imageView.clipsToBounds = true imageView.isUserInteractionEnabled = true + imageView.accessibilityIgnoresInvertColors = true + imageView.isAccessibilityElement = true return imageView }() diff --git a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift index 0102da14..271980ba 100644 --- a/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/TwidereX/Scene/MediaPreview/MediaPreviewViewController.swift @@ -54,6 +54,7 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency { let button = HitTestExpandedButton(type: .system) button.imageView?.tintColor = .label button.setImage(Asset.Editing.xmarkRound.image.withRenderingMode(.alwaysTemplate), for: .normal) + button.accessibilityLabel = L10n.Accessibility.Common.close return button }() @@ -132,53 +133,14 @@ extension MediaPreviewViewController { pageControl.trailingAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.trailingAnchor), pageControl.bottomAnchor.constraint(equalTo: pageControlBackgroundVisualEffectView.bottomAnchor), ]) - -// -// switch viewModel.rootItem { -// case .tweet(let meta): -// pageControl.numberOfPages = viewModel.viewControllers.count -// if case let .tweet(meta) = viewModel.rootItem { -// pageControl.currentPage = meta.initialIndex -// } -// pageControl.isHidden = viewModel.viewControllers.count == 1 -// mediaInfoDescriptionView.actionToolbarContainer.delegate = self -// let managedObjectContext = self.context.managedObjectContext -// managedObjectContext.perform { -// let tweet = managedObjectContext.object(with: meta.tweetObjectID) as! Tweet -// let targetTweet = tweet.retweet ?? tweet -// let activeTwitterAuthenticationBox = self.context.authenticationService.activeTwitterAuthenticationBox.value -// let requestTwitterUserID = activeTwitterAuthenticationBox?.twitterUserID ?? "" -// MediaPreviewViewController.configure(actionToolbarContainer: self.mediaInfoDescriptionView.actionToolbarContainer, tweet: targetTweet, requestTwitterUserID: requestTwitterUserID) -// -// // observe model change -// ManagedObjectObserver.observe(object: tweet.retweet ?? tweet) -// .receive(on: DispatchQueue.main) -// .sink { _ in -// // do nothing -// } receiveValue: { [weak self] change in -// guard let self = self else { return } -// guard case let .update(object) = change.changeType, -// let newTweet = object as? Tweet else { return } -// let targetTweet = newTweet.retweet ?? newTweet -// let activeTwitterAuthenticationBox = self.context.authenticationService.activeTwitterAuthenticationBox.value -// let requestTwitterUserID = activeTwitterAuthenticationBox?.twitterUserID ?? "" -// -// MediaPreviewViewController.configure(actionToolbarContainer: self.mediaInfoDescriptionView.actionToolbarContainer, tweet: targetTweet, requestTwitterUserID: requestTwitterUserID) -// } -// .store(in: &self.disposeBag) -// } -// case .local(let meta): -// pageControl.isHidden = true -// mediaInfoDescriptionView.isHidden = true -// } if let status = viewModel.status { mediaInfoDescriptionView.configure( statusObject: status, - configurationContext: MediaInfoDescriptionView.ConfigurationContext( + configurationContext: .init( dateTimeProvider: DateTimeSwiftProvider(), twitterTextProvider: OfficialTwitterTextProvider(), - activeAuthenticationContext: context.authenticationService.activeAuthenticationContext.eraseToAnyPublisher() + authenticationContext: context.authenticationService.$activeAuthenticationContext ) ) } else { @@ -188,6 +150,7 @@ extension MediaPreviewViewController { pageControl.numberOfPages = viewModel.viewControllers.count pageControl.isHidden = viewModel.viewControllers.count == 1 pageControl.isUserInteractionEnabled = false + pageControl.addTarget(self, action: #selector(MediaPreviewViewController.pageControlValueDidChanged(_:)), for: .valueChanged) viewModel.mediaPreviewImageViewControllerDelegate = self @@ -214,6 +177,17 @@ extension MediaPreviewViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) dismiss(animated: true, completion: nil) } + + @objc private func pageControlValueDidChanged(_ sender: UIPageControl) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + let currentPage = sender.currentPage + guard let pageCount = pageViewController.pageCount, + currentPage >= 0, + currentPage < pageCount + else { return } + + pageViewController.scrollToPage(.at(index: currentPage), animated: true, completion: nil) + } } diff --git a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift index 1aedef78..a3bdb2fb 100644 --- a/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift +++ b/TwidereX/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift @@ -65,5 +65,4 @@ extension MediaPreviewVideoViewModel { let assetURL: URL? let previewURL: URL? } - } diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift index 6b74de02..3559651b 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView+ViewModel.swift @@ -9,8 +9,9 @@ import os.log import UIKit import Combine -import AppShared import CoreDataStack +import AppShared +import TwidereCore import TwitterMeta import MastodonMeta import Meta @@ -19,31 +20,84 @@ extension MediaInfoDescriptionView { final class ViewModel: ObservableObject { var disposeBag = Set() var observations = Set() + + @Published public var platform: Platform = .none + @Published public var twitterTextProvider: TwitterTextProvider? + @Published public var dateTimeProvider: DateTimeProvider? + @Published public var authenticationContext: AuthenticationContext? + @Published public var authorUserIdentifier: UserIdentifier? @Published public var authorAvatarImageURL: URL? @Published public var authorName: MetaContent? - @Published public var protected: Bool = false + @Published public var protected = false + @Published public var isMyself = false @Published public var content: MetaContent? + + @Published public var visibility: StatusVisibility? - @Published public var isRepost: Bool = false - @Published public var isLike: Bool = false + @Published public var isRepost = false + @Published public var isRepostEnabled = true + + @Published public var isLike = false + + init() { + // isMyself + Publishers.CombineLatest( + $authenticationContext, + $authorUserIdentifier + ) + .map { authenticationContext, authorUserIdentifier -> Bool in + guard let authenticationContext = authenticationContext, + let authorUserIdentifier = authorUserIdentifier + else { return false } + let meUserIdentifier = authenticationContext.userIdentifier + switch (meUserIdentifier, authorUserIdentifier) { + case (.twitter(let me), .twitter(let author)): + return me.id == author.id + case (.mastodon(let me), .mastodon(let author)): + return me.domain == author.domain + && me.id == author.id + default: + return false + } + } + .assign(to: &$isMyself) + // isRepostEnabled + Publishers.CombineLatest4( + $platform, + $protected, + $isMyself, + $visibility + ) + .map { platform, protected, isMyself, visibility -> Bool in + switch platform { + case .none: + return true + case .twitter: + guard !isMyself else { return true } + return !protected + case .mastodon: + guard !isMyself else { return true } + guard case let .mastodon(visibility) = visibility else { + return true + } + switch visibility { + case .public, .unlisted: + return true + case .private, .direct, ._other: + return false + } + } + } + .assign(to: &$isRepostEnabled) + } } } extension MediaInfoDescriptionView.ViewModel { func bind(view: MediaInfoDescriptionView) { - // content - $content - .sink { metaContent in - guard let content = metaContent else { - view.contentTextView.reset() - return - } - view.contentTextView.configure(content: content) - } - .store(in: &disposeBag) // avatar $authorAvatarImageURL .sink { url in @@ -75,18 +129,31 @@ extension MediaInfoDescriptionView.ViewModel { view.nameMetaLabel.configure(content: metaContent) } .store(in: &disposeBag) + // content + $content + .sink { metaContent in + guard let content = metaContent else { + view.contentTextView.reset() + return + } + view.contentTextView.configure(content: content) + } + .store(in: &disposeBag) // toolbar + $platform + .assign(to: \.platform, on: view.toolbar.viewModel) + .store(in: &disposeBag) Publishers.CombineLatest( $isRepost, - $protected + $isRepostEnabled ) - .sink { isRepost, protected in - view.toolbar.setupRepost(count: 0, isRepost: isRepost, isLocked: protected) + .sink { isRepost, isEnabled in + view.toolbar.setupRepost(count: 0, isEnabled: isEnabled, isHighlighted: isRepost) } .store(in: &disposeBag) $isLike .sink { isLike in - view.toolbar.setupLike(count: 0, isLike: isLike) + view.toolbar.setupLike(count: 0, isHighlighted: isLike) } .store(in: &disposeBag) } @@ -94,24 +161,9 @@ extension MediaInfoDescriptionView.ViewModel { extension MediaInfoDescriptionView { - public struct ConfigurationContext { - public let dateTimeProvider: DateTimeProvider - public let twitterTextProvider: TwitterTextProvider - public let activeAuthenticationContext: AnyPublisher - - public init( - dateTimeProvider: DateTimeProvider, - twitterTextProvider: TwitterTextProvider, - activeAuthenticationContext: AnyPublisher - ) { - self.dateTimeProvider = dateTimeProvider - self.twitterTextProvider = twitterTextProvider - self.activeAuthenticationContext = activeAuthenticationContext - } - } + public typealias ConfigurationContext = StatusView.ConfigurationContext } - extension MediaInfoDescriptionView { public func configure( statusObject object: StatusObject, @@ -137,24 +189,17 @@ extension MediaInfoDescriptionView { twitterStatus status: TwitterStatus, configurationContext: ConfigurationContext ) { - configureAuthor( - twitterStatus: status, - dateTimeProvider: configurationContext.dateTimeProvider - ) - configureContent( - twitterStatus: status, - twitterTextProvider: configurationContext.twitterTextProvider - ) - configureToolbar( - twitterStatus: status, - activeAuthenticationContext: configurationContext.activeAuthenticationContext - ) + viewModel.platform = .twitter + viewModel.dateTimeProvider = configurationContext.dateTimeProvider + viewModel.twitterTextProvider = configurationContext.twitterTextProvider + configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) + + configureAuthor(twitterStatus: status) + configureContent(twitterStatus: status) + configureToolbar(twitterStatus: status) } - private func configureAuthor( - twitterStatus status: TwitterStatus, - dateTimeProvider: DateTimeProvider - ) { + private func configureAuthor(twitterStatus status: TwitterStatus) { let author = (status.repost ?? status).author // author avatar @@ -173,10 +218,12 @@ extension MediaInfoDescriptionView { .store(in: &disposeBag) } - private func configureContent( - twitterStatus status: TwitterStatus, - twitterTextProvider: TwitterTextProvider - ) { + private func configureContent(twitterStatus status: TwitterStatus) { + guard let twitterTextProvider = viewModel.twitterTextProvider else { + assertionFailure() + return + } + let status = status.repost ?? status let content = TwitterContent(content: status.text) let metaContent = TwitterMetaContent.convert( @@ -185,17 +232,15 @@ extension MediaInfoDescriptionView { twitterTextProvider: twitterTextProvider ) viewModel.content = metaContent + viewModel.visibility = nil } - private func configureToolbar( - twitterStatus status: TwitterStatus, - activeAuthenticationContext: AnyPublisher - ) { + private func configureToolbar(twitterStatus status: TwitterStatus) { let status = status.repost ?? status // relationship Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.repostBy) ) .map { authenticationContext, repostBy in @@ -209,7 +254,7 @@ extension MediaInfoDescriptionView { .store(in: &disposeBag) Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.likeBy) ) .map { authenticationContext, likeBy in @@ -230,20 +275,19 @@ extension MediaInfoDescriptionView { mastodonStatus status: MastodonStatus, configurationContext: ConfigurationContext ) { + viewModel.platform = .mastodon + viewModel.dateTimeProvider = configurationContext.dateTimeProvider + viewModel.twitterTextProvider = configurationContext.twitterTextProvider + configurationContext.authenticationContext.assign(to: \.authenticationContext, on: viewModel).store(in: &disposeBag) + // configureHeader(mastodonStatus: status, mastodonNotification: notification) - configureAuthor(mastodonStatus: status, dateTimeProvider: configurationContext.dateTimeProvider) + configureAuthor(mastodonStatus: status) configureContent(mastodonStatus: status) // configureMedia(mastodonStatus: status) - configureToolbar( - mastodonStatus: status, - activeAuthenticationContext: configurationContext.activeAuthenticationContext - ) + configureToolbar(mastodonStatus: status) } - private func configureAuthor( - mastodonStatus status: MastodonStatus, - dateTimeProvider: DateTimeProvider - ) { + private func configureAuthor(mastodonStatus status: MastodonStatus) { let author = (status.repost ?? status).author // author avatar @@ -285,17 +329,16 @@ extension MediaInfoDescriptionView { assertionFailure(error.localizedDescription) viewModel.content = PlaintextMetaContent(string: "") } + + viewModel.visibility = status.visibility.asStatusVisibility } - private func configureToolbar( - mastodonStatus status: MastodonStatus, - activeAuthenticationContext: AnyPublisher - ) { + private func configureToolbar(mastodonStatus status: MastodonStatus) { let status = status.repost ?? status // relationship Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.repostBy) ) .map { authenticationContext, repostBy in @@ -310,7 +353,7 @@ extension MediaInfoDescriptionView { .store(in: &disposeBag) Publishers.CombineLatest( - activeAuthenticationContext, + viewModel.$authenticationContext, status.publisher(for: \.likeBy) ) .map { authenticationContext, likeBy in diff --git a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift index 059c9284..e3216b54 100644 --- a/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift +++ b/TwidereX/Scene/MediaPreview/View/MediaInfoDescriptionView.swift @@ -38,7 +38,10 @@ final class MediaInfoDescriptionView: UIView { let avatarView: ProfileAvatarView = { let avatarView = ProfileAvatarView() - avatarView.dimension = 32 + avatarView.setup(dimension: .inline) + avatarView.isAccessibilityElement = true + avatarView.accessibilityLabel = L10n.Accessibility.Common.Status.authorAvatar + avatarView.accessibilityHint = L10n.Accessibility.VoiceOver.doubleTapToOpenProfile return avatarView }() @@ -114,7 +117,12 @@ extension MediaInfoDescriptionView { bottomContainerStackView.spacing = 8 bottomContainerStackView.alignment = .center + avatarView.translatesAutoresizingMaskIntoConstraints = false bottomContainerStackView.addArrangedSubview(avatarView) + NSLayoutConstraint.activate([ + avatarView.widthAnchor.constraint(equalToConstant: MediaInfoDescriptionView.avatarImageViewSize.width).priority(.required - 1), + avatarView.heightAnchor.constraint(equalToConstant: MediaInfoDescriptionView.avatarImageViewSize.height).priority(.required - 1), + ]) bottomContainerStackView.addArrangedSubview(nameMetaLabel) toolbar.translatesAutoresizingMaskIntoConstraints = false bottomContainerStackView.addArrangedSubview(toolbar) @@ -168,6 +176,20 @@ extension MediaInfoDescriptionView: StatusToolbarDelegate { } } +extension MediaInfoDescriptionView { + override var accessibilityElements: [Any]? { + get { + return [ + avatarView, + nameMetaLabel, + toolbar, + ] + } + set { } + } + +} + #if DEBUG import SwiftUI diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 31f359ab..46b2b6a8 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -12,6 +12,7 @@ import CoreData import CoreDataStack import TwitterSDK import MastodonSDK +import AppShared extension NotificationTimelineViewModel { @@ -22,7 +23,12 @@ extension NotificationTimelineViewModel { ) { let configuration = NotificationSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - userTableViewCellDelegate: userTableViewCellDelegate + userTableViewCellDelegate: userTableViewCellDelegate, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = NotificationSection.diffableDataSource( tableView: tableView, @@ -123,7 +129,7 @@ extension NotificationTimelineViewModel { // load lastest func loadLatest() async { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } do { switch authenticationContext { case .twitter(let authenticationContext): @@ -158,7 +164,7 @@ extension NotificationTimelineViewModel { // load timeline gap func loadMore(item: NotificationItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } let managedObjectContext = context.managedObjectContext let key = "LoadMore@\(record.objectID)" diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 3cae69f2..3e3feee0 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -48,7 +48,7 @@ extension NotificationTimelineViewModel.LoadOldestState { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 9548b354..d2ad5462 100644 --- a/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -48,7 +48,7 @@ final class NotificationTimelineViewModel { self.fetchedResultsController = FeedFetchedResultsController(managedObjectContext: context.managedObjectContext) // end init - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } let emptyFeedPredicate = Feed.nonePredicate() diff --git a/TwidereX/Scene/Notification/NotificationViewController.swift b/TwidereX/Scene/Notification/NotificationViewController.swift index 91a7b02e..5e047a8d 100644 --- a/TwidereX/Scene/Notification/NotificationViewController.swift +++ b/TwidereX/Scene/Notification/NotificationViewController.swift @@ -60,9 +60,11 @@ extension NotificationViewController { navigationItem.leftBarButtonItem = avatarBarButtonItem avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(NotificationViewController.avatarButtonPressed(_:)), for: .touchUpInside) + avatarBarButtonItem.delegate = self + Publishers.CombineLatest( - context.authenticationService.activeAuthenticationContext, - viewModel.viewDidAppear.eraseToAnyPublisher() + context.authenticationService.$activeAuthenticationContext, + viewModel.viewDidAppear ) .receive(on: DispatchQueue.main) .sink { [weak self] authenticationContext, _ in @@ -174,3 +176,7 @@ extension NotificationViewController { scrollToPage(.at(index: index), animated: true, completion: nil) } } + +// MARK: - AvatarBarButtonItemDelegate +extension NotificationViewController: AvatarBarButtonItemDelegate { } + diff --git a/TwidereX/Scene/Notification/NotificationViewModel.swift b/TwidereX/Scene/Notification/NotificationViewModel.swift index d6f41c19..f70f8f0d 100644 --- a/TwidereX/Scene/Notification/NotificationViewModel.swift +++ b/TwidereX/Scene/Notification/NotificationViewModel.swift @@ -31,7 +31,7 @@ final class NotificationViewModel { self._coordinator = coordinator // end init - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .receive(on: DispatchQueue.main) .sink { [weak self] authenticationContext in guard let self = self else { return } diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift index 4dade20d..46c64c52 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeView.swift @@ -10,6 +10,7 @@ import os.log import SwiftUI import Introspect import TwidereAsset +import TwidereLocalization struct WelcomeView: View { @@ -32,7 +33,7 @@ struct WelcomeView: View { HStack { // not use UIImage init method here // only .init(_:bundle:) works with the dynamic Dark Mode asset - Image(Asset.Scene.Welcome.twidere.name, bundle: TwidereAsset.bundle) + Image(decorative: Asset.Scene.Welcome.twidere.name, bundle: TwidereAsset.bundle) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 48, height: 48) @@ -74,6 +75,7 @@ struct WelcomeView: View { } } ) + .accessibilityHint(L10n.Accessibility.Scene.SignIn.pleaseEnterMastodonDomainToSignIn) .textFieldStyle(PlainTextFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) @@ -129,7 +131,7 @@ struct TwitterAuthenticateButton: View { primaryAction() } label: { HStack { - Image(Asset.Logo.twitter.name, bundle: TwidereAsset.bundle) + Image(decorative: Asset.Logo.twitter.name, bundle: TwidereAsset.bundle) .renderingMode(.template) Spacer() if isBusy { @@ -147,9 +149,12 @@ struct TwitterAuthenticateButton: View { Button { secondaryAction() } label: { - Image(Asset.Editing.ellipsis.name, bundle: TwidereAsset.bundle) - .renderingMode(.template) - .padding(.horizontal, 18) + Image( + Asset.Editing.ellipsis.name, bundle: TwidereAsset.bundle, + label: Text(L10n.Accessibility.Scene.SignIn.twitterClientAuthenticationKeySetting) + ) + .renderingMode(.template) + .padding(.horizontal, 18) } } .frame(height: 48) @@ -171,7 +176,7 @@ struct MastodonAuthenticateButton: View { primaryAction() } label: { HStack { - Image(Asset.Logo.mastodon.name, bundle: TwidereAsset.bundle) + Image(decorative: Asset.Logo.mastodon.name, bundle: TwidereAsset.bundle) .renderingMode(.template) Spacer() if isBusy { @@ -183,7 +188,7 @@ struct MastodonAuthenticateButton: View { .frame(maxWidth: .infinity, alignment: .leading) } Spacer() - Image(Asset.Arrows.arrowRight.name, bundle: TwidereAsset.bundle) + Image(decorative: Asset.Arrows.arrowRight.name, bundle: TwidereAsset.bundle) .renderingMode(.template) } .padding(.horizontal, 18) diff --git a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift index 077bea04..38c64b61 100644 --- a/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/TwidereX/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -20,7 +20,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - let logger = Logger(subsystem: "WelcomeViewController", category: "UI") + let logger = Logger(subsystem: "WelcomeViewController", category: "ViewController") var disposeBag = Set() var viewModel: WelcomeViewModel! @@ -28,12 +28,17 @@ final class WelcomeViewController: UIViewController, NeedsDependency { private var twitterAuthenticationController: TwitterAuthenticationController? private var mastodonAuthenticationController: MastodonAuthenticationController? - private(set) lazy var closeBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(WelcomeViewController.closeBarButtonItemPressed(_:))) + private(set) lazy var closeBarButtonItem: UIBarButtonItem = { + let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(WelcomeViewController.closeBarButtonItemPressed(_:))) + item.accessibilityLabel = L10n.Accessibility.Common.close + return item + }() private(set) lazy var backBarButtonItem: UIBarButtonItem = { let image = Asset.Arrows.arrowLeft.image.withRenderingMode(.alwaysTemplate) let item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(WelcomeViewController.backBarButtonItemPressed(_:))) item.tintColor = .label + item.accessibilityLabel = L10n.Accessibility.Common.back return item }() diff --git a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift index c4c8a198..14fdbe1a 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FollowingListViewModel+State.swift @@ -55,7 +55,7 @@ extension FriendshipListViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift b/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift index 7770b0bd..295ffa92 100644 --- a/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift +++ b/TwidereX/Scene/Profile/FriendshipList/FriendshipListViewModel.swift @@ -63,7 +63,7 @@ final class FriendshipListViewModel: NSObject { context: AppContext, kind: Kind ) { - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return nil } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return nil } let userIdentifier = authenticationContext.userIdentifier self.init( diff --git a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift index 18966e99..b5011e77 100644 --- a/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/TwidereX/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -52,7 +52,7 @@ final class ProfileHeaderView: UIView { let avatarView: ProfileAvatarView = { let avatarView = ProfileAvatarView() - avatarView.dimension = ProfileHeaderView.avatarViewSize.width + avatarView.setup(dimension: .plain) return avatarView }() @@ -135,6 +135,8 @@ extension ProfileHeaderView { NSLayoutConstraint.activate([ avatarView.centerXAnchor.constraint(equalTo: centerXAnchor), avatarView.centerYAnchor.constraint(equalTo: bannerContainer.bottomAnchor), + avatarView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarViewSize.width).priority(.required - 1), + avatarView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarViewSize.height).priority(.required - 1), ]) // container: V - [ name container | usernameLabel | friendshipButton | bioTextAreaView | … ] diff --git a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewController.swift b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewController.swift index c74438bd..fee011c1 100644 --- a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewController.swift +++ b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewController.swift @@ -179,29 +179,6 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView } -//// MARK: - AVPlayerViewControllerDelegate -//extension UserTimelineViewController: AVPlayerViewControllerDelegate { -// -// func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator) -// } -// -// func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { -// handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator) -// } -// -//} - -// MARK: - TimelinePostTableViewCellDelegate -//extension UserTimelineViewController: TimelinePostTableViewCellDelegate { -// weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self } -// func parent() -> UIViewController { return self } -//} -// -//// MARK: - TimelineHeaderTableViewCellDelegate -//extension UserTimelineViewController: TimelineHeaderTableViewCellDelegate { } - - // MARK: - CustomScrollViewContainerController extension UserTimelineViewController: ScrollViewContainer { var scrollView: UIScrollView { return tableView } diff --git a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+Diffable.swift b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+Diffable.swift index 797782d1..892ce4c0 100644 --- a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+Diffable.swift @@ -11,6 +11,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import AppShared extension UserTimelineViewModel { func setupDiffableDataSource( @@ -19,7 +20,12 @@ extension UserTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+State.swift b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+State.swift index 6f977ea6..13b742f7 100644 --- a/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+State.swift +++ b/TwidereX/Scene/Profile/HomeTimeline/UserTimelineViewModel+State.swift @@ -176,7 +176,7 @@ extension UserTimelineViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let userIdentifier = viewModel.userIdentifier, - let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+Diffable.swift index 5524dc00..f47bfa2c 100644 --- a/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+Diffable.swift @@ -10,6 +10,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import AppShared extension UserLikeTimelineViewModel { func setupDiffableDataSource( @@ -18,8 +19,14 @@ extension UserLikeTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) + diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, diff --git a/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+State.swift b/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+State.swift index eb8bdd43..380de235 100644 --- a/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+State.swift +++ b/TwidereX/Scene/Profile/LikeTimeline/UserLikeTimelineViewModel+State.swift @@ -163,7 +163,7 @@ extension UserLikeTimelineViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let userIdentifier = viewModel.userIdentifier, - let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Profile/MeProfileViewModel.swift b/TwidereX/Scene/Profile/MeProfileViewModel.swift index c452d9fb..44a1cf59 100644 --- a/TwidereX/Scene/Profile/MeProfileViewModel.swift +++ b/TwidereX/Scene/Profile/MeProfileViewModel.swift @@ -17,7 +17,7 @@ final class MeProfileViewModel: ProfileViewModel { override init(context: AppContext) { super.init(context: context) - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } Task { diff --git a/TwidereX/Scene/Profile/MediaTimeline/UserMediaTimelineViewModel+State.swift b/TwidereX/Scene/Profile/MediaTimeline/UserMediaTimelineViewModel+State.swift index ec07b526..93498762 100644 --- a/TwidereX/Scene/Profile/MediaTimeline/UserMediaTimelineViewModel+State.swift +++ b/TwidereX/Scene/Profile/MediaTimeline/UserMediaTimelineViewModel+State.swift @@ -168,7 +168,7 @@ extension UserMediaTimelineViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } guard let userIdentifier = viewModel.userIdentifier, - let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Profile/ProfileViewController.swift b/TwidereX/Scene/Profile/ProfileViewController.swift index f04ec05f..aface37c 100644 --- a/TwidereX/Scene/Profile/ProfileViewController.swift +++ b/TwidereX/Scene/Profile/ProfileViewController.swift @@ -122,10 +122,11 @@ extension ProfileViewController { if navigationController?.viewControllers.first == self { navigationItem.leftBarButtonItem = avatarBarButtonItem avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(ProfileViewController.avatarButtonPressed(_:)), for: .touchUpInside) + avatarBarButtonItem.delegate = self Publishers.CombineLatest( - context.authenticationService.activeAuthenticationContext, - viewModel.viewDidAppear.eraseToAnyPublisher() + context.authenticationService.$activeAuthenticationContext, + viewModel.viewDidAppear ) .receive(on: DispatchQueue.main) .sink { [weak self] authenticationContext, _ in @@ -181,7 +182,7 @@ extension ProfileViewController { Publishers.CombineLatest3( viewModel.relationshipViewModel.$optionSet, // update trigger viewModel.$userRecord, - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext ) .receive(on: DispatchQueue.main) .sink { [weak self] optionSet, userRecord, authenticationContext in @@ -292,8 +293,11 @@ extension ProfileViewController { apiService: context.apiService, authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) ) coordinator.present(scene: .compose(viewModel: composeViewModel, contentViewModel: composeContentViewModel), from: self, transition: .modal(animated: true, completion: nil)) @@ -301,12 +305,15 @@ extension ProfileViewController { } +// MARK: - AvatarBarButtonItemDelegate +extension ProfileViewController: AvatarBarButtonItemDelegate { } + // MARK: - ProfileHeaderViewControllerDelegate extension ProfileViewController: ProfileHeaderViewControllerDelegate { func headerViewController(_ viewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, friendshipButtonDidPressed button: UIButton) { guard let user = viewModel.user else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } guard let relationshipOptionSet = viewModel.relationshipViewModel.optionSet else { return } let record = UserRecord(object: user) diff --git a/TwidereX/Scene/Profile/ProfileViewModel.swift b/TwidereX/Scene/Profile/ProfileViewModel.swift index e769fcdd..d62f86de 100644 --- a/TwidereX/Scene/Profile/ProfileViewModel.swift +++ b/TwidereX/Scene/Profile/ProfileViewModel.swift @@ -61,7 +61,7 @@ class ProfileViewModel: ObservableObject { .assign(to: &$userIdentifier) // bind active authentication - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } Task { @@ -85,7 +85,7 @@ class ProfileViewModel: ObservableObject { // observe friendship Publishers.CombineLatest( $userRecord, - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext ) .sink { [weak self] userRecord, authenticationContext in guard let self = self else { return } diff --git a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift index 0e419e41..10ad0226 100644 --- a/TwidereX/Scene/Profile/RemoteProfileViewModel.swift +++ b/TwidereX/Scene/Profile/RemoteProfileViewModel.swift @@ -25,7 +25,7 @@ final class RemoteProfileViewModel: ProfileViewModel { setup(user: record) case .twitter(let twitterContext): Task { - guard case let .twitter(authenticationContext) = context.authenticationService.activeAuthenticationContext.value else { return } + guard case let .twitter(authenticationContext) = context.authenticationService.activeAuthenticationContext else { return } do { let _record = try await fetchTwitterUser( twitterContext: twitterContext, diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift index cdacf9bc..6127b121 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewController.swift @@ -81,7 +81,7 @@ extension SavedSearchViewController: UITableViewDelegate { guard let diffableDataSource = self.viewModel.diffableDataSource, case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath), - let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext.value + let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext else { return nil } let deleteAction = UIContextualAction( diff --git a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift index edd4d929..696bf352 100644 --- a/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift +++ b/TwidereX/Scene/Search/SavedSearch/SavedSearchViewModel.swift @@ -32,7 +32,7 @@ final class SavedSearchViewModel { self.savedSearchFetchedResultController = SavedSearchFetchedResultController(managedObjectContext: context.managedObjectContext) // end init - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .map { $0?.userIdentifier } .assign(to: \.userIdentifier, on: savedSearchFetchedResultController) .store(in: &disposeBag) diff --git a/TwidereX/Scene/Search/Search/SearchViewController.swift b/TwidereX/Scene/Search/Search/SearchViewController.swift index 9998cecf..4fa78d69 100644 --- a/TwidereX/Scene/Search/Search/SearchViewController.swift +++ b/TwidereX/Scene/Search/Search/SearchViewController.swift @@ -123,7 +123,7 @@ extension SearchViewController { Publishers.CombineLatest3( viewModel.$savedSearchTexts, searchResultViewModel.$searchText, - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext ) .receive(on: DispatchQueue.main) .sink { [weak self] texts, searchText, activeAuthenticationContext in @@ -235,7 +235,7 @@ extension SearchViewController: UITableViewDelegate { guard let diffableDataSource = self.viewModel.diffableDataSource, case let .history(record) = diffableDataSource.itemIdentifier(for: indexPath), - let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext.value + let authenticationContext = self.viewModel.context.authenticationService.activeAuthenticationContext else { return nil } let deleteAction = UIContextualAction( diff --git a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift index 88c9b01c..79f0a30a 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel+Diffable.swift @@ -75,7 +75,7 @@ extension SearchViewModel { Publishers.CombineLatest3( historyItems, trendItems, - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext ) .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] historyItems, trendItems, authenticationContext in diff --git a/TwidereX/Scene/Search/Search/SearchViewModel.swift b/TwidereX/Scene/Search/Search/SearchViewModel.swift index aaa8077f..30993614 100644 --- a/TwidereX/Scene/Search/Search/SearchViewModel.swift +++ b/TwidereX/Scene/Search/Search/SearchViewModel.swift @@ -37,7 +37,7 @@ final class SearchViewModel { viewDidAppear .sink { [weak self] _ in guard let self = self else { return } - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } Task { do { @@ -56,7 +56,7 @@ final class SearchViewModel { ) .sink { [weak self] trendGroupIndex, _ in guard let self = self else { return } - guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = self.context.authenticationService.activeAuthenticationContext else { return } Task { do { diff --git a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift index ca6f283b..64a158d1 100644 --- a/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/Hashtag/SearchHashtagViewModel+State.swift @@ -71,7 +71,7 @@ extension SearchHashtagViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Search/SearchResult/Media/SearchMediaViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/Media/SearchMediaViewModel+State.swift index dd5b5d72..dd3e923c 100644 --- a/TwidereX/Scene/Search/SearchResult/Media/SearchMediaViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/Media/SearchMediaViewModel+State.swift @@ -75,7 +75,7 @@ extension SearchMediaViewModel.State { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift index 64bab2e4..641a910f 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewController.swift @@ -126,7 +126,7 @@ extension SearchResultViewController: UISearchBarDelegate { func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") guard let searchText = searchBar.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchText.isEmpty else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } Task { try await DataSourceFacade.responseToCreateSavedSearch( diff --git a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift index 5256cc96..ba2c8f6a 100644 --- a/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift +++ b/TwidereX/Scene/Search/SearchResult/SearchResultViewModel.swift @@ -31,7 +31,7 @@ final class SearchResultViewModel { self.context = context self._coordinator = coordinator - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .receive(on: DispatchQueue.main) .sink { [weak self] authenticationContext in guard let self = self else { return } diff --git a/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+Diffable.swift b/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+Diffable.swift index b8ac3e26..6e87fddb 100644 --- a/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+Diffable.swift @@ -10,6 +10,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import AppShared extension SearchTimelineViewModel { @MainActor func setupDiffableDataSource( @@ -18,7 +19,12 @@ extension SearchTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+State.swift index 8ac69c0c..5fee8d3e 100644 --- a/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/Status/SearchTimelineViewModel+State.swift @@ -72,7 +72,7 @@ extension SearchTimelineViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift index 297bdff9..6c3de0b0 100644 --- a/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift +++ b/TwidereX/Scene/Search/SearchResult/User/SearchUserViewModel+State.swift @@ -72,7 +72,7 @@ extension SearchUserViewModel.State { } guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { stateMachine.enter(Fail.self) return diff --git a/TwidereX/Scene/Search/Trend/TrendViewModel.swift b/TwidereX/Scene/Search/Trend/TrendViewModel.swift index b1dfe68a..1c9b4ce1 100644 --- a/TwidereX/Scene/Search/Trend/TrendViewModel.swift +++ b/TwidereX/Scene/Search/Trend/TrendViewModel.swift @@ -31,7 +31,7 @@ final class TrendViewModel { self.trendService = TrendService(apiService: context.apiService) // end init - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] authenticationContext in diff --git a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift index cdc58ca5..b45749cd 100644 --- a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift +++ b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem+ViewModel.swift @@ -10,42 +10,93 @@ import UIKit import Combine import TwidereCore import CoreDataStack +import TwidereUI extension AvatarBarButtonItem { public class ViewModel: ObservableObject { - var bindDisposeBag = Set() + var disposeBag = Set() var observations = Set() + @Published var name: String? + @Published var username: String? @Published var avatarURL: URL? + + @Published var avatarStyle: UserDefaults.AvatarStyle = UserDefaults.shared.avatarStyle + + init() { + UserDefaults.shared + .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in + self.avatarStyle = defaults.avatarStyle + } + .store(in: &observations) + } } } extension AvatarBarButtonItem.ViewModel { func bind(view: AvatarBarButtonItem) { - // avatar + bindAvatar(view: view) + bindAccessibility(view: view) + } + + private func bindAvatar(view: AvatarBarButtonItem) { $avatarURL .sink { avatarURL in let configuration = AvatarImageView.Configuration(url: avatarURL) view.avatarButton.avatarImageView.configure(configuration: configuration) } - .store(in: &bindDisposeBag) - UserDefaults.shared - .observe(\.avatarStyle, options: [.initial, .new]) { defaults, _ in - let avatarStyle = defaults.avatarStyle + .store(in: &disposeBag) + + func cornerConfiguration(avatarStyle: UserDefaults.AvatarStyle) -> AvatarImageView.CornerConfiguration { + switch avatarStyle { + case .circle: + return .init(corner: .circle) + case .roundedSquare: + return .init(corner: .scale(ratio: 4)) + } + } + + view.avatarButton.avatarImageView.configure( + cornerConfiguration: cornerConfiguration(avatarStyle: avatarStyle) + ) + + $avatarStyle + .removeDuplicates() + .sink { avatarStyle in + let cornerConfiguration = cornerConfiguration(avatarStyle: avatarStyle) + let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters()) animator.addAnimations { [weak view] in guard let view = view else { return } - switch avatarStyle { - case .circle: - view.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .circle)) - case .roundedSquare: - view.avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .scale(ratio: 4))) - } + view.avatarButton.avatarImageView.configure(cornerConfiguration: cornerConfiguration) } animator.startAnimation() } - .store(in: &observations) + .store(in: &disposeBag) + } + + private func bindAccessibility(view: AvatarBarButtonItem) { + Publishers.CombineLatest( + $name, + $username + ) + .sink { name, username in + let info = [name, username] + .compactMap { $0 } + .joined(separator: ", ") + + guard !info.isEmpty else { + view.avatarButton.accessibilityLabel = nil + return + } + + view.accessibilityLabel = L10n.Accessibility.Scene.ManageAccounts.currentSignInUser(info) + } + .store(in: &disposeBag) + + view.accessibilityHint = L10n.Accessibility.VoiceOver.doubleTapAndHoldToOpenTheAccountsPanel } + } extension AvatarBarButtonItem { @@ -76,6 +127,16 @@ extension AvatarBarButtonItem { return } + user.publisher(for: \.name) + .map { $0 as String? } + .assign(to: \.name, on: viewModel) + .store(in: &disposeBag) + + user.publisher(for: \.username) + .map { $0 as String? } + .assign(to: \.username, on: viewModel) + .store(in: &disposeBag) + // avatar user.publisher(for: \.profileImageURL) .map { _ in user.avatarImageURL() } @@ -88,6 +149,16 @@ extension AvatarBarButtonItem { return } + user.publisher(for: \.displayName) + .map { _ in user.displayName as String? } + .assign(to: \.name, on: viewModel) + .store(in: &disposeBag) + + user.publisher(for: \.acct) + .map { _ in user.acctWithDomain as String? } + .assign(to: \.username, on: viewModel) + .store(in: &disposeBag) + // avatar user.publisher(for: \.avatar) .map { avatar in avatar.flatMap { URL(string: $0) } } diff --git a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift index d504c436..3f99ee08 100644 --- a/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift +++ b/TwidereX/Scene/Share/View/Button/AvatarBarButtonItem.swift @@ -6,15 +6,23 @@ // Copyright © 2020 Twidere. All rights reserved. // +import os.log import UIKit import Combine import TwidereUI +public protocol AvatarBarButtonItemDelegate: AnyObject { + func avatarBarButtonItem(_ barButtonItem: AvatarBarButtonItem, didLongPressed sender: UILongPressGestureRecognizer) +} + public final class AvatarBarButtonItem: UIBarButtonItem { + let logger = Logger(subsystem: "AvatarBarButtonItem", category: "View") + var disposeBag = Set() + weak var delegate: AvatarBarButtonItemDelegate? - public static let avatarButtonSize = CGSize(width: 30, height: 30) + public static let size = CGSize(width: 30, height: 30) private(set) lazy var viewModel: ViewModel = { let viewModel = ViewModel() @@ -26,8 +34,8 @@ public final class AvatarBarButtonItem: UIBarButtonItem { let button = AvatarButton() button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - button.widthAnchor.constraint(equalToConstant: avatarButtonSize.width).priority(.required - 1), - button.heightAnchor.constraint(equalToConstant: avatarButtonSize.height).priority(.required - 1), + button.widthAnchor.constraint(equalToConstant: size.width).priority(.required - 1), + button.heightAnchor.constraint(equalToConstant: size.height).priority(.required - 1), ]) return button }() @@ -48,6 +56,25 @@ extension AvatarBarButtonItem { private func _init() { customView = avatarButton + + let avatarButtonLongPressGestureRecognizer = UILongPressGestureRecognizer() + avatarButtonLongPressGestureRecognizer.addTarget(self, action: #selector(AvatarBarButtonItem.avatarButtonDidLongPressed(_:))) + avatarButton.addGestureRecognizer(avatarButtonLongPressGestureRecognizer) + } + +} + +extension AvatarBarButtonItem { + + @objc func avatarButtonDidLongPressed(_ sender: UILongPressGestureRecognizer) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + switch sender.state { + case .began: + delegate?.avatarBarButtonItem(self, didLongPressed: sender) + default: + break + } } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift index 0bf10e76..fda8b17e 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell+ViewModel.swift @@ -11,8 +11,10 @@ import Combine import SwiftUI import CoreDataStack import AppShared +import TwidereUI extension StatusTableViewCell { + final class ViewModel { enum Value { case feed(Feed) @@ -22,20 +24,16 @@ extension StatusTableViewCell { } let value: Value - let activeAuthenticationContext: AnyPublisher - init( - value: Value, - activeAuthenticationContext: AnyPublisher - ) { + init(value: Value) { self.value = value - self.activeAuthenticationContext = activeAuthenticationContext } } func configure( tableView: UITableView, viewModel: ViewModel, + configurationContext: StatusView.ConfigurationContext, delegate: StatusViewTableViewCellDelegate? ) { if statusView.frame == .zero { @@ -50,12 +48,6 @@ extension StatusTableViewCell { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") } - let configurationContext = StatusView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - activeAuthenticationContext: viewModel.activeAuthenticationContext - ) - switch viewModel.value { case .feed(let feed): statusView.configure( @@ -84,19 +76,21 @@ extension StatusTableViewCell { configureSeparator(style: .inset) } + self.delegate = delegate - statusView.viewModel.contentRevealChangePublisher + statusView.viewModel.$isContentReveal + .removeDuplicates() + .dropFirst() .receive(on: DispatchQueue.main) - .sink { [weak tableView] _ in + .sink { [weak tableView, weak self] _ in guard let tableView = tableView else { return } + guard let _ = self else { return } UIView.setAnimationsEnabled(false) tableView.beginUpdates() tableView.endUpdates() UIView.setAnimationsEnabled(true) } .store(in: &disposeBag) - - self.delegate = delegate } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift index af2f38b8..2a1b6a4d 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusTableViewCell.swift @@ -9,13 +9,14 @@ import os.log import UIKit import Combine +import TwidereUI class StatusTableViewCell: UITableViewCell { private var _disposeBag = Set() var disposeBag = Set() - let logger = Logger(subsystem: "StatusTableViewCell", category: "UI") + let logger = Logger(subsystem: "StatusTableViewCell", category: "View") weak var delegate: StatusViewTableViewCellDelegate? @@ -83,7 +84,6 @@ extension StatusTableViewCell { // a11y isAccessibilityElement = true - accessibilityElements = [statusView] statusView.viewModel.$groupedAccessibilityLabel .receive(on: DispatchQueue.main) .sink { [weak self] accessibilityLabel in diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift index ba10bb68..04e879f4 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell+ViewModel.swift @@ -12,8 +12,10 @@ import SwiftUI import CoreDataStack import TwidereCore import AppShared +import TwidereUI extension StatusThreadRootTableViewCell { + final class ViewModel { enum Value { case statusObject(StatusObject) @@ -22,20 +24,18 @@ extension StatusThreadRootTableViewCell { } let value: Value - let activeAuthenticationContext: AnyPublisher init( - value: Value, - activeAuthenticationContext: AnyPublisher + value: Value ) { self.value = value - self.activeAuthenticationContext = activeAuthenticationContext } } func configure( tableView: UITableView, viewModel: StatusThreadRootTableViewCell.ViewModel, + configurationContext: StatusView.ConfigurationContext, delegate: StatusViewTableViewCellDelegate? ) { if statusView.frame == .zero { @@ -48,13 +48,7 @@ extension StatusThreadRootTableViewCell { statusView.quoteStatusView?.contentTextView.preferredMaxLayoutWidth = statusView.quoteStatusView?.contentMaxLayoutWidth logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell") } - - let configurationContext = StatusView.ConfigurationContext( - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider(), - activeAuthenticationContext: viewModel.activeAuthenticationContext - ) - + switch viewModel.value { case .statusObject(let object): statusView.configure( @@ -74,18 +68,21 @@ extension StatusThreadRootTableViewCell { ) } - - statusView.viewModel.contentRevealChangePublisher + self.delegate = delegate + + statusView.viewModel.$isContentReveal + .removeDuplicates() + .dropFirst() .receive(on: DispatchQueue.main) - .sink { [weak tableView] _ in + .sink { [weak tableView, weak self] _ in guard let tableView = tableView else { return } + guard let _ = self else { return } UIView.setAnimationsEnabled(false) tableView.beginUpdates() tableView.endUpdates() UIView.setAnimationsEnabled(true) } .store(in: &disposeBag) - - self.delegate = delegate } + } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift index 11922a18..47c63bfe 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusThreadRootTableViewCell.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine +import TwidereUI final class StatusThreadRootTableViewCell: UITableViewCell { @@ -91,21 +92,34 @@ extension StatusThreadRootTableViewCell { isAccessibilityElement = false } +} + +extension StatusThreadRootTableViewCell { + override var accessibilityElements: [Any]? { get { - return [ + let elements: [UIView?] = [ statusView.headerTextLabel, statusView.authorAvatarButton, statusView.authorNameLabel, + statusView.authorUsernameLabel, + statusView.visibilityImageView, statusView.spoilerContentTextView, + statusView.expandContentButton, statusView.contentTextView, statusView.mediaGridContainerView, + statusView.pollTableView, + statusView.pollVoteDescriptionLabel, + statusView.pollVoteButton, statusView.quoteStatusView, + statusView.locationLabel, statusView.metricsDashboardView, statusView.toolbar, ] - .compactMap { $0 } - .filter { !$0.isHidden } + + return elements + .compactMap { $0 } + .filter { !$0.isHidden } } set { } } diff --git a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift index 15e85886..af108290 100644 --- a/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift +++ b/TwidereX/Scene/Share/View/TableViewCell/StatusViewTableViewCellDelegate.swift @@ -23,7 +23,7 @@ protocol StatusViewContainerTableViewCell: UITableViewCell, AutoGenerateProtocol // sourcery: protocolName = "StatusViewDelegate" // sourcery: replaceOf = "statusView(_" // sourcery: replaceWith = "func tableViewCell(_ cell: UITableViewCell," -protocol StatusViewTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate { +protocol StatusViewTableViewCellDelegate: AutoGenerateProtocolDelegate { // sourcery:inline:StatusViewTableViewCellDelegate.AutoGenerateProtocolDelegate func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, headerDidPressed header: UIView) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton) diff --git a/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewController.swift b/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewController.swift index 537ba08e..8d4f0088 100644 --- a/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewController.swift +++ b/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewController.swift @@ -102,7 +102,7 @@ extension DrawerSidebarViewController { settingCollectionView: settingCollectionView ) - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } let user = authenticationContext?.user(in: self.context.managedObjectContext) diff --git a/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewModel+Diffable.swift b/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewModel+Diffable.swift index 6fc94fcb..3d21c018 100644 --- a/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewModel+Diffable.swift +++ b/TwidereX/Scene/Sidebar/Drawer/DrawerSidebarViewModel+Diffable.swift @@ -20,7 +20,7 @@ extension DrawerSidebarViewModel { sidebarSnapshot.appendSections([.main]) sidebarDiffableDataSource?.applySnapshotUsingReloadData(sidebarSnapshot) - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } var snapshot = NSDiffableDataSourceSnapshot() diff --git a/TwidereX/Scene/Sidebar/Drawer/View/DrawerSidebarHeaderView.swift b/TwidereX/Scene/Sidebar/Drawer/View/DrawerSidebarHeaderView.swift index 5dc2af07..5d5b0f51 100644 --- a/TwidereX/Scene/Sidebar/Drawer/View/DrawerSidebarHeaderView.swift +++ b/TwidereX/Scene/Sidebar/Drawer/View/DrawerSidebarHeaderView.swift @@ -39,7 +39,7 @@ final class DrawerSidebarHeaderView: UIView { let avatarView: ProfileAvatarView = { let avatarView = ProfileAvatarView() - avatarView.dimension = DrawerSidebarHeaderView.avatarViewSize.width + avatarView.setup(dimension: .inline) return avatarView }() @@ -120,8 +120,8 @@ extension DrawerSidebarHeaderView { avatarView.translatesAutoresizingMaskIntoConstraints = false infoStackView.addArrangedSubview(avatarView) NSLayoutConstraint.activate([ - avatarView.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1), - avatarView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), + avatarView.widthAnchor.constraint(equalToConstant: DrawerSidebarHeaderView.avatarViewSize.width).priority(.required - 1), + avatarView.heightAnchor.constraint(equalToConstant: DrawerSidebarHeaderView.avatarViewSize.height).priority(.required - 1), ]) let nameStackView = UIStackView() diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift index b79f0091..abafba71 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+Diffable.swift @@ -11,6 +11,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import AppShared extension StatusThreadViewModel { @@ -20,8 +21,14 @@ extension StatusThreadViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: nil + timelineMiddleLoaderTableViewCellDelegate: nil, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) + diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, context: context, diff --git a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift index 37a8064e..726534b8 100644 --- a/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift +++ b/TwidereX/Scene/StatusThread/StatusThreadViewModel+LoadThreadState.swift @@ -96,7 +96,7 @@ extension StatusThreadViewModel.LoadThreadState { if twitterConversation.conversationID == nil { // fetch conversationID if not exist - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value?.twitterAuthenticationContext else { + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext else { await enter(state: PrepareFail.self) return } @@ -107,7 +107,7 @@ extension StatusThreadViewModel.LoadThreadState { authenticationContext: authenticationContext ) guard let conversationID = response.value.data?.first?.conversationID else { - assertionFailure() + // assertionFailure() await enter(state: PrepareFail.self) return } @@ -247,7 +247,7 @@ extension StatusThreadViewModel.LoadThreadState { twitterConversation: StatusThreadViewModel.ThreadContext.TwitterConversation ) async -> [TwitterStatusThreadLeafViewModel.Node] { guard let viewModel = viewModel, let stateMachine = stateMachine else { return [] } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value?.twitterAuthenticationContext, + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.twitterAuthenticationContext, let conversationID = twitterConversation.conversationID else { await enter(state: Fail.self) @@ -336,7 +336,7 @@ extension StatusThreadViewModel.LoadThreadState { descendantNodes: [] ) } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value?.mastodonAuthenticationContext + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext?.mastodonAuthenticationContext else { await enter(state: Fail.self) return MastodonContextResponse( diff --git a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift index 610de87c..c8b98111 100644 --- a/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift +++ b/TwidereX/Scene/StatusThread/Twitter/TwitterStatusThreadReplyViewModel+State.swift @@ -220,7 +220,7 @@ extension TwitterStatusThreadReplyViewModel.State { super.didEnter(from: previousState) guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value, + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext, case let .twitter(twitterAuthenticationContext) = authenticationContext else { return diff --git a/TwidereX/Scene/Timeline/Base/TimelineViewController.swift b/TwidereX/Scene/Timeline/Base/TimelineViewController.swift index 83b59f71..cf369b50 100644 --- a/TwidereX/Scene/Timeline/Base/TimelineViewController.swift +++ b/TwidereX/Scene/Timeline/Base/TimelineViewController.swift @@ -98,10 +98,11 @@ extension TimelineViewController { self.avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(TimelineViewController.avatarButtonPressed(_:)), for: .touchUpInside) } .store(in: &disposeBag) + avatarBarButtonItem.delegate = self // bind avatarBarButtonItem data Publishers.CombineLatest( - context.authenticationService.activeAuthenticationContext, + context.authenticationService.$activeAuthenticationContext, viewModel.viewDidAppear.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) @@ -259,8 +260,11 @@ extension TimelineViewController { apiService: context.apiService, authenticationService: context.authenticationService, mastodonEmojiService: context.mastodonEmojiService, - dateTimeProvider: DateTimeSwiftProvider(), - twitterTextProvider: OfficialTwitterTextProvider() + statusViewConfigureContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) ) coordinator.present(scene: .compose(viewModel: composeViewModel, contentViewModel: composeContentViewModel), from: self, transition: .modal(animated: true, completion: nil)) @@ -295,36 +299,11 @@ extension TimelineViewController: UITableViewDelegate, AutoGenerateTableViewDele } // sourcery:end - -// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { -// guard let diffableDataSource = viewModel.diffableDataSource else { return 100 } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return 100 } -// -// guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else { -// return 200 -// } -// // os_log("%{public}s[%{public}ld], %{public}s: cache cell frame %s", ((#file as NSString).lastPathComponent), #line, #function, frame.debugDescription) -// -// return ceil(frame.height) -// } -// -// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// handleTableView(tableView, willDisplay: cell, forRowAt: indexPath) -// } -// -// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { -// handleTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath) -// -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// -// let key = item.hashValue -// let frame = cell.frame -// viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key)) -// } - } +// MARK: - AvatarBarButtonItemDelegate +extension TimelineViewController: AvatarBarButtonItemDelegate { } + // MARK: - ScrollViewContainer extension TimelineViewController: ScrollViewContainer { diff --git a/TwidereX/Scene/Timeline/Base/TimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Base/TimelineViewModel+Diffable.swift index 9a60100d..d38c7974 100644 --- a/TwidereX/Scene/Timeline/Base/TimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Base/TimelineViewModel+Diffable.swift @@ -77,7 +77,7 @@ extension TimelineViewModel { defer { isLoadingLatest = false } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } do { switch authenticationContext { case .twitter(let authenticationContext): @@ -111,7 +111,7 @@ extension TimelineViewModel { // load timeline gap func loadMore(item: StatusItem) async { guard case let .feedLoader(record) = item else { return } - guard let authenticationContext = context.authenticationService.activeAuthenticationContext.value else { return } + guard let authenticationContext = context.authenticationService.activeAuthenticationContext else { return } guard let diffableDataSource = diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() diff --git a/TwidereX/Scene/Timeline/Base/TimelineViewModel+LoadOldestState.swift b/TwidereX/Scene/Timeline/Base/TimelineViewModel+LoadOldestState.swift index 5bdd7fde..8f8955e6 100644 --- a/TwidereX/Scene/Timeline/Base/TimelineViewModel+LoadOldestState.swift +++ b/TwidereX/Scene/Timeline/Base/TimelineViewModel+LoadOldestState.swift @@ -90,7 +90,7 @@ extension TimelineViewModel.LoadOldestState { private func fetch(anchor record: StatusRecord) async { guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext.value else { + guard let authenticationContext = viewModel.context.authenticationService.activeAuthenticationContext else { await enter(state: Fail.self) return } diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift index beffdd75..5a9017e8 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel+Diffable.swift @@ -11,6 +11,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import TwidereCore +import AppShared extension FederatedTimelineViewModel { @@ -21,7 +23,12 @@ extension FederatedTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate + timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, + statusViewConfigurationContext: StatusView.ConfigurationContext( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift index e6e976b6..e035c63a 100644 --- a/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Federated/FederatedTimelineViewModel.swift @@ -21,7 +21,7 @@ final class FederatedTimelineViewModel: TimelineViewModel { kind: .federated(local: local) ) - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } guard let authenticationContext = authenticationContext else { diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift index 57b46f0c..637e578e 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewController+DebugAction.swift @@ -250,7 +250,7 @@ extension HomeTimelineViewController { case blockingAuthor func match(item: StatusItem) -> Bool { - let authenticationContext = AppContext.shared.authenticationService.activeAuthenticationContext.value + let authenticationContext = AppContext.shared.authenticationService.activeAuthenticationContext switch item { case .feed(let record): guard let feed = record.object(in: AppContext.shared.managedObjectContext) else { return false } diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift index ceae35a3..13885a48 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel+Diffable.swift @@ -11,6 +11,7 @@ import UIKit import Combine import CoreData import CoreDataStack +import AppShared extension HomeTimelineViewModel { @@ -21,7 +22,12 @@ extension HomeTimelineViewModel { ) { let configuration = StatusSection.Configuration( statusViewTableViewCellDelegate: statusViewTableViewCellDelegate, - timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate + timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate, + statusViewConfigurationContext: .init( + dateTimeProvider: DateTimeSwiftProvider(), + twitterTextProvider: OfficialTwitterTextProvider(), + authenticationContext: context.authenticationService.$activeAuthenticationContext + ) ) diffableDataSource = StatusSection.diffableDataSource( tableView: tableView, diff --git a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift index 9ed513b6..447b0244 100644 --- a/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift +++ b/TwidereX/Scene/Timeline/Home/HomeTimelineViewModel.swift @@ -14,7 +14,7 @@ final class HomeTimelineViewModel: TimelineViewModel { init(context: AppContext) { super.init(context: context, kind: .home) - context.authenticationService.activeAuthenticationContext + context.authenticationService.$activeAuthenticationContext .sink { [weak self] authenticationContext in guard let self = self else { return } let emptyFeedPredicate = Feed.nonePredicate() diff --git a/TwidereX/Template/AutoGenerateProtocolDelegate.swift b/TwidereX/Template/AutoGenerateProtocolDelegate.swift index d38ee646..1dab93d5 100644 --- a/TwidereX/Template/AutoGenerateProtocolDelegate.swift +++ b/TwidereX/Template/AutoGenerateProtocolDelegate.swift @@ -7,4 +7,4 @@ import Foundation -@objc protocol AutoGenerateProtocolDelegate { } +@objc protocol AutoGenerateProtocolDelegate: AnyObject { } diff --git a/TwidereXTests/Info.plist b/TwidereXTests/Info.plist index 37ac53d2..24413898 100644 --- a/TwidereXTests/Info.plist +++ b/TwidereXTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.4 + 1.2.6 CFBundleVersion - 74 + 77 diff --git a/TwidereXUITests/Info.plist b/TwidereXUITests/Info.plist index 37ac53d2..24413898 100644 --- a/TwidereXUITests/Info.plist +++ b/TwidereXUITests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.4 + 1.2.6 CFBundleVersion - 74 + 77