diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 4c76ebcf8..16c084cec 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 diff --git a/AppShared/UserDefaults+Notification.swift b/AppShared/UserDefaults+Notification.swift new file mode 100644 index 000000000..e743e70a0 --- /dev/null +++ b/AppShared/UserDefaults+Notification.swift @@ -0,0 +1,40 @@ +// +// UserDefaults+Notification.swift +// AppShared +// +// Created by Cirno MainasuK on 2021-10-9. +// + +import UIKit +import CryptoKit + +extension UserDefaults { + // always use hash value (SHA256) from accessToken as key + private static func deriveKey(from accessToken: String, prefix: String) -> String { + let digest = SHA256.hash(data: Data(accessToken.utf8)) + let bytes = [UInt8](digest) + let hex = bytes.toHexString() + let key = prefix + "@" + hex + return key + } + + private static let notificationCountKeyPrefix = "notification_count" + + public func getNotificationCountWithAccessToken(accessToken: String) -> Int { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + return integer(forKey: key) + } + + public func setNotificationCountWithAccessToken(accessToken: String, value: Int) { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + setValue(value, forKey: key) + } + + public func increaseNotificationCount(accessToken: String) { + let count = getNotificationCountWithAccessToken(accessToken: accessToken) + setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1) + } + +} diff --git a/AppShared/UserDefaults.swift b/AppShared/UserDefaults.swift index 67a3cf685..753a3284f 100644 --- a/AppShared/UserDefaults.swift +++ b/AppShared/UserDefaults.swift @@ -6,39 +6,8 @@ // import UIKit -import CryptoKit extension UserDefaults { public static let shared = UserDefaults(suiteName: AppName.groupID)! } -extension UserDefaults { - // always use hash value (SHA256) from accessToken as key - private static func deriveKey(from accessToken: String, prefix: String) -> String { - let digest = SHA256.hash(data: Data(accessToken.utf8)) - let bytes = [UInt8](digest) - let hex = bytes.toHexString() - let key = prefix + "@" + hex - return key - } - - private static let notificationCountKeyPrefix = "notification_count" - - public func getNotificationCountWithAccessToken(accessToken: String) -> Int { - let prefix = UserDefaults.notificationCountKeyPrefix - let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) - return integer(forKey: key) - } - - public func setNotificationCountWithAccessToken(accessToken: String, value: Int) { - let prefix = UserDefaults.notificationCountKeyPrefix - let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) - setValue(value, forKey: key) - } - - public func increaseNotificationCount(accessToken: String) { - let count = getNotificationCountWithAccessToken(accessToken: accessToken) - setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1) - } - -} diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents index 670241f35..6d576ca15 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData 2.xcdatamodel/contents @@ -63,6 +63,13 @@ + + + + + + + @@ -75,6 +82,7 @@ + @@ -191,7 +199,6 @@ - @@ -281,7 +288,7 @@ - + @@ -289,10 +296,11 @@ - + + \ No newline at end of file diff --git a/CoreDataStack/Entity/Instance.swift b/CoreDataStack/Entity/Instance.swift new file mode 100644 index 000000000..8976097ef --- /dev/null +++ b/CoreDataStack/Entity/Instance.swift @@ -0,0 +1,70 @@ +// +// Instance.swift +// CoreDataStack +// +// Created by Cirno MainasuK on 2021-10-9. +// + +import Foundation +import CoreData + +public final class Instance: NSManagedObject { + @NSManaged public var domain: String + + @NSManaged public private(set) var createdAt: Date + @NSManaged public private(set) var updatedAt: Date + + @NSManaged public private(set) var configurationRaw: Data? + + // MARK: one-to-many relationships + @NSManaged public var authentications: Set +} + +extension Instance { + public override func awakeFromInsert() { + super.awakeFromInsert() + let now = Date() + setPrimitiveValue(now, forKey: #keyPath(Instance.createdAt)) + setPrimitiveValue(now, forKey: #keyPath(Instance.updatedAt)) + } + + @discardableResult + public static func insert( + into context: NSManagedObjectContext, + property: Property + ) -> Instance { + let instance: Instance = context.insertObject() + instance.domain = property.domain + return instance + } + + public func update(configurationRaw: Data?) { + self.configurationRaw = configurationRaw + } + + public func didUpdate(at networkDate: Date) { + self.updatedAt = networkDate + } +} + +extension Instance { + public struct Property { + public let domain: String + + public init(domain: String) { + self.domain = domain + } + } +} + +extension Instance: Managed { + public static var defaultSortDescriptors: [NSSortDescriptor] { + return [NSSortDescriptor(keyPath: \Instance.createdAt, ascending: false)] + } +} + +extension Instance { + public static func predicate(domain: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(Instance.domain), domain) + } +} diff --git a/CoreDataStack/Entity/MastodonAuthentication.swift b/CoreDataStack/Entity/MastodonAuthentication.swift index 0ee0e343b..7aafd65a4 100644 --- a/CoreDataStack/Entity/MastodonAuthentication.swift +++ b/CoreDataStack/Entity/MastodonAuthentication.swift @@ -30,6 +30,9 @@ final public class MastodonAuthentication: NSManagedObject { // one-to-one relationship @NSManaged public private(set) var user: MastodonUser + // many-to-one relationship + @NSManaged public private(set) var instance: Instance? + } extension MastodonAuthentication { @@ -97,6 +100,12 @@ extension MastodonAuthentication { } } + public func update(instance: Instance) { + if self.instance != instance { + self.instance = instance + } + } + public func didUpdate(at networkDate: Date) { self.updatedAt = networkDate } @@ -143,7 +152,7 @@ extension MastodonAuthentication: Managed { extension MastodonAuthentication { - static func predicate(domain: String) -> NSPredicate { + public static func predicate(domain: String) -> NSPredicate { return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain) } @@ -158,4 +167,8 @@ extension MastodonAuthentication { ]) } + public static func predicate(userAccessToken: String) -> NSPredicate { + return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userAccessToken), userAccessToken) + } + } diff --git a/CoreDataStack/Entity/Setting.swift b/CoreDataStack/Entity/Setting.swift index 27971157f..94ee50959 100644 --- a/CoreDataStack/Entity/Setting.swift +++ b/CoreDataStack/Entity/Setting.swift @@ -13,7 +13,7 @@ public final class Setting: NSManagedObject { @NSManaged public var domain: String @NSManaged public var userID: String - @NSManaged public var appearanceRaw: String +// @NSManaged public var appearanceRaw: String @NSManaged public var preferredTrueBlackDarkMode: Bool @NSManaged public var preferredStaticAvatar: Bool @NSManaged public var preferredStaticEmoji: Bool @@ -41,17 +41,17 @@ extension Setting { property: Property ) -> Setting { let setting: Setting = context.insertObject() - setting.appearanceRaw = property.appearanceRaw +// setting.appearanceRaw = property.appearanceRaw setting.domain = property.domain setting.userID = property.userID return setting } - public func update(appearanceRaw: String) { - guard appearanceRaw != self.appearanceRaw else { return } - self.appearanceRaw = appearanceRaw - didUpdate(at: Date()) - } +// public func update(appearanceRaw: String) { +// guard appearanceRaw != self.appearanceRaw else { return } +// self.appearanceRaw = appearanceRaw +// didUpdate(at: Date()) +// } public func update(preferredTrueBlackDarkMode: Bool) { guard preferredTrueBlackDarkMode != self.preferredTrueBlackDarkMode else { return } @@ -87,12 +87,16 @@ extension Setting { public struct Property { public let domain: String public let userID: String - public let appearanceRaw: String +// public let appearanceRaw: String - public init(domain: String, userID: String, appearanceRaw: String) { + public init( + domain: String, + userID: String +// appearanceRaw: String + ) { self.domain = domain self.userID = userID - self.appearanceRaw = appearanceRaw +// self.appearanceRaw = appearanceRaw } } } diff --git a/CoreDataStack/Info.plist b/CoreDataStack/Info.plist index 4c76ebcf8..16c084cec 100644 --- a/CoreDataStack/Info.plist +++ b/CoreDataStack/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 diff --git a/CoreDataStackTests/Info.plist b/CoreDataStackTests/Info.plist index 4c76ebcf8..16c084cec 100644 --- a/CoreDataStackTests/Info.plist +++ b/CoreDataStackTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 diff --git a/Localization/StringsConvertor/Intents/input/ar_SA/Intents.strings b/Localization/StringsConvertor/Intents/input/ar_SA/Intents.strings index bf3e77ed2..cde27dc97 100644 --- a/Localization/StringsConvertor/Intents/input/ar_SA/Intents.strings +++ b/Localization/StringsConvertor/Intents/input/ar_SA/Intents.strings @@ -1,22 +1,22 @@ -"16wxgf" = "Post on Mastodon"; +"16wxgf" = "النَشر على ماستودون"; "751xkl" = "محتوى نصي"; "CsR7G2" = "انشر على ماستدون"; -"HZSGTr" = "What content to post?"; +"HZSGTr" = "ما المُحتوى المُراد نشره؟"; -"HdGikU" = "Posting failed"; +"HdGikU" = "فَشَلَ النشر"; "KDNTJ4" = "سبب الإخفاق"; -"RHxKOw" = "Send Post with text content"; +"RHxKOw" = "إرسال مَنشور يَحوي نص"; -"RxSqsb" = "Post"; +"RxSqsb" = "مَنشور"; -"WCIR3D" = "Post ${content} on Mastodon"; +"WCIR3D" = "نَشر ${content} على ماستودون"; -"ZKJSNu" = "Post"; +"ZKJSNu" = "مَنشور"; "ZS1XaK" = "${content}"; @@ -24,13 +24,13 @@ "Zo4jgJ" = "مدى ظهور المنشور"; -"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; +"apSxMG-dYQ5NN" = "هُناك عدد ${count} خِيار مُطابق لِـ\"عام\"."; -"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; +"apSxMG-ehFLjY" = "هُناك عدد ${count} خِيار مُطابق لِـ\"المُتابِعُون فقط\"."; -"ayoYEb-dYQ5NN" = "${content}, Public"; +"ayoYEb-dYQ5NN" = "${content}، عام"; -"ayoYEb-ehFLjY" = "${content}, Followers Only"; +"ayoYEb-ehFLjY" = "${content}، المُتابِعُون فقط"; "dUyuGg" = "النشر على ماستدون"; @@ -38,13 +38,13 @@ "ehFLjY" = "لمتابعيك فقط"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "فَشَلَ النشر، ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "تمَّ إرسال المنشور بِنجاح."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "للتأكيد، هل تَريد \"عام\"؟"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "للتأكيد، هل تُريد \"للمُتابِعين فقط\"؟"; "rM6dvp" = "عنوان URL"; diff --git a/Localization/StringsConvertor/Intents/input/ar_SA/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/ar_SA/Intents.stringsdict index f273a551d..e44e666ae 100644 --- a/Localization/StringsConvertor/Intents/input/ar_SA/Intents.stringsdict +++ b/Localization/StringsConvertor/Intents/input/ar_SA/Intents.stringsdict @@ -5,7 +5,7 @@ There are ${count} options matching ‘${content}’. - 2 NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${content}’. + هُناك %#@count_option@ تتطابق مَعَ '${content}'. count_option NSStringFormatSpecTypeKey @@ -13,23 +13,23 @@ NSStringFormatValueTypeKey %ld zero - %ld options + لا خيار one - 1 option + خيار واحد two - %ld options + خياران few - %ld options + %ld خيارات many - %ld options + %ld خيارًا other - %ld options + %ld خيار There are ${count} options matching ‘${visibility}’. NSStringLocalizedFormatKey - There are %#@count_option@ matching ‘${visibility}’. + هُناك %#@count_option@ تتطابق مَعَ '${visibility}'. count_option NSStringFormatSpecTypeKey @@ -37,17 +37,17 @@ NSStringFormatValueTypeKey %ld zero - %ld options + لا خيار one - 1 option + خيار واحد two - %ld options + خياران few - %ld options + %ld خيارات many - %ld options + %ld خيارًا other - %ld options + %ld خيار diff --git a/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings new file mode 100644 index 000000000..3e1c69fc3 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Di Mastodon de biweşîne"; + +"751xkl" = "Naveroka nivîsê"; + +"CsR7G2" = "Di Mastodon de biweşîne"; + +"HZSGTr" = "Kîjan naverok bila bê şandin?"; + +"HdGikU" = "Şandin têkçû"; + +"KDNTJ4" = "Sedema têkçûnê"; + +"RHxKOw" = "Bi naveroka nivîsî şandiyan bişîne"; + +"RxSqsb" = "Şandî"; + +"WCIR3D" = "${content} biweşîne di Mastodon de"; + +"ZKJSNu" = "Şandî"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Xuyanî"; + +"Zo4jgJ" = "Xuyaniya şandiyê"; + +"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Giştî' ne."; + +"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopandin' hene."; + +"ayoYEb-dYQ5NN" = "${content}, Giştî"; + +"ayoYEb-ehFLjY" = "${content}, Tenê şopînêr"; + +"dUyuGg" = "Li ser Mastodon bişînin"; + +"dYQ5NN" = "Gelemperî"; + +"ehFLjY" = "Tenê şopîneran"; + +"gfePDu" = "Weşandin bi ser neket. ${failureReason}"; + +"k7dbKQ" = "Şandî bi serkeftî hate şandin."; + +"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, we 'Giştî' dixwest?"; + +"oGiqmY-ehFLjY" = "Tenê ji bo piştrastkirinê, we 'Tenê Şopdarên' dixwest?"; + +"rM6dvp" = "Girêdan"; + +"ryJLwG" = "Bi serkeftî hat şandin. "; diff --git a/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.stringsdict new file mode 100644 index 000000000..2f001aaa9 --- /dev/null +++ b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.stringsdict @@ -0,0 +1,38 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + %#@count_option@ heye ku bi ‘${content}’ re têkildar e. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 vebijêrk + other + %ld vebijêrk + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + %#@count_option@ heye ku bi ‘${visibility}’ re têkildar e. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + one + 1 vebijêrk + other + %ld vebijêrk + + + + diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift index 124612e5c..6507986be 100644 --- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift +++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift @@ -51,6 +51,7 @@ private func map(language: String) -> String? { case "fr_FR": return "fr" // French case "de_DE": return "de" // German case "ja_JP": return "ja" // Japanese + case "kmr_TR": return "ku-TR" // Kurmanji (Kurdish) case "ru_RU": return "ru" // Russian case "gd_GB": return "gd-GB" // Scottish Gaelic case "es_ES": return "es" // Spanish diff --git a/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict b/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict index e6b0d5f95..e3dee0d80 100644 --- a/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict @@ -15,21 +15,21 @@ zero %ld unread notification one - 1 unread notification + إشعار واحِد غير مقروء two - %ld unread notification + إشعاران غير مقروءان few %ld unread notification many - %ld unread notification + %ld إشعارًا غيرَ مقروء other - %ld unread notification + %ld إشعار غير مقروء a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ + تمَّ تجاوز حدّ الإدخال %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -37,23 +37,23 @@ NSStringFormatValueTypeKey ld zero - %ld characters + لا حرف one - 1 character + حرفٌ واحِد two - %ld characters + حرفان اثنان few - %ld characters + %ld حُرُوف many - %ld characters + %ld حرفًا other - %ld characters + %ld حَرف a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Input limit remains %#@character_count@ + يتبقَّى على حدّ الإدخال %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -61,17 +61,17 @@ NSStringFormatValueTypeKey ld zero - %ld characters + لا حرف one - 1 character + حرفٌ واحِد two - %ld characters + حرفان اثنان few - %ld characters + %ld حُرُوف many - %ld characters + %ld حرفًا other - %ld characters + %ld حَرف plural.count.metric_formatted.post @@ -85,17 +85,17 @@ NSStringFormatValueTypeKey ld zero - posts + لا منشور one - post + منشور two - posts + منشوران few - posts + منشورات many - posts + منشورًا other - posts + منشور plural.count.post @@ -109,17 +109,17 @@ NSStringFormatValueTypeKey ld zero - %ld posts + لا منشور one - 1 post + منشورٌ واحِد two - %ld posts + منشورانِ اثنان few - %ld posts + %ld منشورات many - %ld posts + %ld منشورًا other - %ld posts + %ld منشور plural.count.favorite @@ -133,17 +133,17 @@ NSStringFormatValueTypeKey ld zero - %ld favorites + لا إعجاب one - 1 favorite + إعجابٌ واحِد two - %ld favorites + إعجابانِ اثنان few - %ld favorites + %ld إعجابات many - %ld favorites + %ld إعجابًا other - %ld favorites + %ld إعجاب plural.count.reblog @@ -157,17 +157,17 @@ NSStringFormatValueTypeKey ld zero - %ld reblogs + لا إعاد تدوين one - 1 reblog + إعادةُ تدوينٍ واحِدة two - %ld reblogs + إعادتا تدوين few - %ld reblogs + %ld إعاداتِ تدوين many - %ld reblogs + %ld إعادةٍ للتدوين other - %ld reblogs + %ld إعادة تدوين plural.count.vote @@ -181,17 +181,17 @@ NSStringFormatValueTypeKey ld zero - %ld votes + لا صوت one - 1 vote + صوتٌ واحِد two - %ld votes + صوتانِ اثنان few - %ld votes + %ld أصوات many - %ld votes + %ld صوتًا other - %ld votes + %ld صوت plural.count.voter @@ -205,17 +205,17 @@ NSStringFormatValueTypeKey ld zero - %ld voters + لا مُصوِّتون one - 1 voter + مُصوِّتٌ واحِد two - %ld voters + مُصوِّتانِ اثنان few - %ld voters + %ld مُصوِّتين many - %ld voters + %ld مُصوِّتًا other - %ld voters + %ld مُصوِّت plural.people_talking @@ -229,17 +229,17 @@ NSStringFormatValueTypeKey ld zero - %ld people talking + لا أحَدَ يتحدَّث one - 1 people talking + شخصٌ واحدٌ يتحدَّث two - %ld people talking + شخصانِ اثنان يتحدَّثا few - %ld people talking + %ld أشخاصٍ يتحدَّثون many - %ld people talking + %ld شخصًا يتحدَّثون other - %ld people talking + %ld شخصٍ يتحدَّثون plural.count.following @@ -253,17 +253,17 @@ NSStringFormatValueTypeKey ld zero - %ld following + لا مُتابَع one - 1 following + مُتابَعٌ واحد two - %ld following + مُتابَعانِ few - %ld following + %ld مُتابَعين many - %ld following + %ld مُتابَعًا other - %ld following + %ld مُتابَع plural.count.follower @@ -279,15 +279,15 @@ zero %ld followers one - 1 follower + مُتابِعٌ واحد two - %ld followers + مُتابِعانِ اثنان few - %ld followers + %ld مُتابِعين many - %ld followers + %ld مُتابِعًا other - %ld followers + %ld مُتابِع date.year.left @@ -301,17 +301,17 @@ NSStringFormatValueTypeKey ld zero - %ld years left + تتبقى لَحظة one - 1 year left + تتبقى سنة two - %ld years left + تتبقى سنتين few - %ld years left + تتبقى %ld سنوات many - %ld years left + تتبقى %ld سنةً other - %ld years left + تتبقى %ld سنة date.month.left @@ -325,17 +325,17 @@ NSStringFormatValueTypeKey ld zero - %ld months left + تتبقى لَحظة one - 1 months left + يتبقى شهر two - %ld months left + يتبقى شهرين few - %ld months left + يتبقى %ld أشهر many - %ld months left + يتبقى %ld شهرًا other - %ld months left + يتبقى %ld شهر date.day.left @@ -349,17 +349,17 @@ NSStringFormatValueTypeKey ld zero - %ld days left + تتبقى لحظة one - 1 day left + يتبقى يوم two - %ld days left + يتبقى يومين few - %ld days left + يتبقى %ld أيام many - %ld days left + يتبقى %ld يومًا other - %ld days left + يتبقى %ld يوم date.hour.left @@ -373,17 +373,17 @@ NSStringFormatValueTypeKey ld zero - %ld hours left + تتبقى لَحظة one - 1 hour left + تتبقى ساعة two - %ld hours left + تتبقى ساعتين few - %ld hours left + تتبقى %ld ساعات many - %ld hours left + تتبقى %ld ساعةً other - %ld hours left + تتبقى %ld ساعة date.minute.left @@ -397,17 +397,17 @@ NSStringFormatValueTypeKey ld zero - %ld minutes left + تتبقى لَحظة one - 1 minute left + تتبقى دقيقة two - %ld minutes left + تتبقى دقيقتين few - %ld minutes left + تتبقى %ld دقائق many - %ld minutes left + تتبقى %ld دقيقةً other - %ld minutes left + تتبقى %ld دقيقة date.second.left @@ -421,17 +421,17 @@ NSStringFormatValueTypeKey ld zero - %ld seconds left + تتبقى لَحظة one - 1 second left + تتبقى ثانية two - %ld seconds left + تتبقى ثانيتين few - %ld seconds left + تتبقى %ld ثوان many - %ld seconds left + تتبقى %ld ثانيةً other - %ld seconds left + تتبقى %ld ثانية date.year.ago.abbr @@ -445,17 +445,17 @@ NSStringFormatValueTypeKey ld zero - %ldy ago + مُنذُ لَحظة one - 1y ago + مُنذُ سنة two - %ldy ago + مُنذُ سنتين few - %ldy ago + مُنذُ %ld سنين many - %ldy ago + مُنذُ %ld سنةً other - %ldy ago + مُنذُ %ld سنة date.month.ago.abbr @@ -469,17 +469,17 @@ NSStringFormatValueTypeKey ld zero - %ldM ago + مُنذُ لَحظة one - 1M ago + مُنذُ شهر two - %ldM ago + مُنذُ شهرين few - %ldM ago + مُنذُ %ld أشهُر many - %ldM ago + مُنذُ %ld شهرًا other - %ldM ago + مُنذُ %ld شهر date.day.ago.abbr @@ -493,17 +493,17 @@ NSStringFormatValueTypeKey ld zero - %ldd ago + مُنذُ لَحظة one - 1d ago + مُنذُ يوم two - %ldd ago + مُنذُ يومين few - %ldd ago + مُنذُ %ld أيام many - %ldd ago + مُنذُ %ld يومًا other - %ldd ago + مُنذُ %ld يوم date.hour.ago.abbr @@ -517,17 +517,17 @@ NSStringFormatValueTypeKey ld zero - %ldh ago + مُنذُ لَحظة one - 1h ago + مُنذُ ساعة two - %ldh ago + مُنذُ ساعتين few - %ldh ago + مُنذُ %ld ساعات many - %ldh ago + مُنذُ %ld ساعةًَ other - %ldh ago + مُنذُ %ld ساعة date.minute.ago.abbr @@ -541,17 +541,17 @@ NSStringFormatValueTypeKey ld zero - %ldm ago + مُنذُ لَحظة one - 1m ago + مُنذُ دقيقة two - %ldm ago + مُنذُ دقيقتان few - %ldm ago + مُنذُ %ld دقائق many - %ldm ago + مُنذُ %ld دقيقةً other - %ldm ago + مُنذُ %ld دقيقة date.second.ago.abbr @@ -565,17 +565,17 @@ NSStringFormatValueTypeKey ld zero - %lds ago + مُنذُ لَحظة one - 1s ago + مُنذُ ثانية two - %lds ago + مُنذُ ثانيتين few - %lds ago + مُنذُ %ld ثوان many - %lds ago + مُنذُ %ld ثانية other - %lds ago + مُنذُ %ld ثانية diff --git a/Localization/StringsConvertor/input/ar_SA/app.json b/Localization/StringsConvertor/input/ar_SA/app.json index db8de394d..4bf55d918 100644 --- a/Localization/StringsConvertor/input/ar_SA/app.json +++ b/Localization/StringsConvertor/input/ar_SA/app.json @@ -2,8 +2,8 @@ "common": { "alerts": { "common": { - "please_try_again": "الرجاء المحاولة مرة أخرى.", - "please_try_again_later": "الرجاء المحاولة مرة أخرى لاحقاً." + "please_try_again": "يُرجى المحاولة مرة أُخرى.", + "please_try_again_later": "يُرجى المحاولة مرة أُخرى لاحقاً." }, "sign_up_failure": { "title": "فشل التسجيل" @@ -28,8 +28,8 @@ } }, "edit_profile_failure": { - "title": "Edit Profile Error", - "message": "لا يمكن تعديل الملف الشخصي. الرجاء المحاولة مرة أخرى." + "title": "خطأ في تَحرير الملف الشخصي", + "message": "لا يمكن تعديل الملف الشخصي. يُرجى المحاولة مرة أُخرى." }, "sign_out": { "title": "تسجيل الخروج", @@ -49,8 +49,8 @@ "delete": "احذف" }, "clean_cache": { - "title": "تنظيف ذاكرة التخزين المؤقت", - "message": "تم تنظيف ذاكرة التخزين المؤقت %s بنجاح." + "title": "مَحو ذاكرة التخزين المؤقت", + "message": "تمَّ مَحو ذاكرة التخزين المؤقت %s بنجاح." } }, "controls": { @@ -64,17 +64,17 @@ "edit": "تعديل", "save": "حفظ", "ok": "حسنًا", - "done": "تم", + "done": "تمّ", "confirm": "تأكيد", "continue": "واصل", "cancel": "إلغاء", "discard": "تجاهل", - "try_again": "حاول مرة أخرى", + "try_again": "المُحاولة مرة أُخرى", "take_photo": "التقط صورة", "save_photo": "حفظ الصورة", "copy_photo": "نسخ الصورة", - "sign_in": "لِج", - "sign_up": "انشئ حسابًا", + "sign_in": "تسجيل الدخول", + "sign_up": "إنشاء حِساب", "see_more": "عرض المزيد", "preview": "معاينة", "share": "شارك", @@ -122,7 +122,7 @@ } }, "status": { - "user_reblogged": "%s reblogged", + "user_reblogged": "أعادَ %s تدوينها", "user_replied_to": "رد على %s", "show_post": "اظهر المنشور", "show_user_profile": "اظهر الملف التعريفي للمستخدم", @@ -152,8 +152,8 @@ "friendship": { "follow": "اتبع", "following": "مُتابَع", - "request": "Request", - "pending": "Pending", + "request": "إرسال طَلَب", + "pending": "قيد المُراجعة", "block": "حظر", "block_user": "حظر %s", "block_domain": "حظر %s", @@ -168,12 +168,12 @@ "edit_info": "تعديل المعلومات" }, "timeline": { - "filtered": "Filtered", + "filtered": "مُصفَّى", "timestamp": { "now": "الأن" }, "loader": { - "load_missing_posts": "Load missing posts", + "load_missing_posts": "تحميل المنشورات المَفقودة", "loading_missing_posts": "تحميل المزيد من المنشورات...", "show_more_replies": "إظهار المزيد من الردود" }, @@ -194,19 +194,19 @@ "slogan": "Social networking\nback in your hands." }, "server_picker": { - "title": "Pick a server,\nany server.", + "title": "اِختر خادِم،\nأي خادِم.", "button": { "category": { "all": "الكل", "all_accessiblity_description": "الفئة: الكل", - "academia": "academia", + "academia": "أكاديمي", "activism": "للنشطاء", "food": "الطعام", "furry": "فروي", "games": "ألعاب", "general": "عام", "journalism": "صحافة", - "lgbt": "lgbt", + "lgbt": "مجتمع الشواذ", "regional": "اقليمي", "art": "فن", "music": "موسيقى", @@ -302,20 +302,20 @@ "dont_receive_email": { "title": "تحقق من بريدك الإلكتروني", "description": "Check if your email address is correct as well as your junk folder if you haven’t.", - "resend_email": "Resend Email" + "resend_email": "إعادة إرسال البريد الإلكتروني" }, "open_email_app": { - "title": "Check your inbox.", + "title": "تحقَّق من بريدك الوارِد.", "description": "We just sent you an email. Check your junk folder if you haven’t.", "mail": "البريد", - "open_email_client": "Open Email Client" + "open_email_client": "فتح عميل البريد الإلكتروني" } }, "home_timeline": { "title": "الخيط الرئيسي", "navigation_bar_state": { "offline": "غير متصل", - "new_posts": "See new posts", + "new_posts": "إظهار منشورات جديدة", "published": "تم نشره!", "Publishing": "جارٍ نشر المشاركة…" } @@ -334,7 +334,7 @@ "photo_library": "مكتبة الصور", "browse": "تصفح" }, - "content_input_placeholder": "ما الذي يجول ببالك", + "content_input_placeholder": "أخبِرنا بِما يَجُولُ فِي ذِهنَك", "compose_action": "انشر", "replying_to_user": "رد على %s", "attachment": { @@ -367,7 +367,7 @@ "space_to_add": "Space to add" }, "accessibility": { - "append_attachment": "Add Attachment", + "append_attachment": "إضافة مُرفَق", "append_poll": "اضافة استطلاع رأي", "remove_poll": "إزالة الاستطلاع", "custom_emoji_picker": "منتقي مخصص للإيموجي", @@ -376,11 +376,11 @@ "post_visibility_menu": "Post Visibility Menu" }, "keyboard": { - "discard_post": "Discard Post", - "publish_post": "Publish Post", - "toggle_poll": "Toggle Poll", - "toggle_content_warning": "Toggle Content Warning", - "append_attachment_entry": "Add Attachment - %s", + "discard_post": "تجاهُل المنشور", + "publish_post": "نَشر المَنشُور", + "toggle_poll": "تبديل الاستطلاع", + "toggle_content_warning": "تبديل تحذير المُحتوى", + "append_attachment_entry": "إضافة مُرفَق - %s", "select_visibility_entry": "اختر مدى الظهور - %s" } }, @@ -393,7 +393,7 @@ "fields": { "add_row": "إضافة صف", "placeholder": { - "label": "Label", + "label": "التسمية", "content": "المحتوى" } }, @@ -424,7 +424,7 @@ "hash_tag": { "title": "ذات شعبية على ماستدون", "description": "Hashtags that are getting quite a bit of attention", - "people_talking": "%s people are talking" + "people_talking": "%s أشخاص يتحدَّثوا" }, "accounts": { "title": "حسابات قد تعجبك", @@ -459,15 +459,15 @@ "user_reblogged_your_post": "أعاد %s تدوين مشاركتك", "user_mentioned_you": "أشار إليك %s", "user_requested_to_follow_you": "طلب %s متابعتك", - "user_your_poll_has_ended": "%s Your poll has ended", + "user_your_poll_has_ended": "%s اِنتهى استطلاعُكَ للرأي", "keyobard": { "show_everything": "إظهار كل شيء", - "show_mentions": "Show Mentions" + "show_mentions": "إظهار الإشارات" } }, "thread": { - "back_title": "Post", - "title": "Post from %s" + "back_title": "منشور", + "title": "مَنشور مِن %s" }, "settings": { "title": "الإعدادات", @@ -475,29 +475,29 @@ "appearance": { "title": "المظهر", "automatic": "تلقائي", - "light": "Always Light", - "dark": "Always Dark" + "light": "مضيءٌ دائمًا", + "dark": "مظلمٌ دائِمًا" }, "notifications": { "title": "الإشعارات", - "favorites": "Favorites my post", + "favorites": "الإعجاب بِمنشوراتي", "follows": "يتابعني", - "boosts": "Reblogs my post", - "mentions": "Mentions me", + "boosts": "إعادة تدوين منشوراتي", + "mentions": "الإشارة لي", "trigger": { - "anyone": "anyone", + "anyone": "أي شخص", "follower": "مشترِك", - "follow": "anyone I follow", - "noone": "no one", - "title": "Notify me when" + "follow": "أي شخص أُتابِعُه", + "noone": "لا أحد", + "title": "إشعاري عِندَ" } }, "preference": { "title": "التفضيلات", - "true_black_dark_mode": "True black dark mode", - "disable_avatar_animation": "Disable animated avatars", - "disable_emoji_animation": "Disable animated emojis", - "using_default_browser": "Use default browser to open links" + "true_black_dark_mode": "النمط الأسود الداكِن الحقيقي", + "disable_avatar_animation": "تعطيل الصور الرمزية المتحرِّكة", + "disable_emoji_animation": "تعطيل الرموز التعبيرية المتحرِّكَة", + "using_default_browser": "اِستخدام المتصفح الافتراضي لفتح الروابط" }, "boring_zone": { "title": "المنطقة المملة", @@ -537,13 +537,13 @@ }, "account_list": { "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", - "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "dismiss_account_switcher": "تجاهُل مبدِّل الحساب", + "add_account": "إضافة حساب" }, "wizard": { - "new_in_mastodon": "New in Mastodon", + "new_in_mastodon": "جديد في ماستودون", "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "accessibility_hint": "انقر نقرًا مزدوجًا لتجاهل النافذة المنبثقة" } } } \ No newline at end of file diff --git a/Localization/StringsConvertor/input/ar_SA/ios-infoPlist.json b/Localization/StringsConvertor/input/ar_SA/ios-infoPlist.json index 54fb1aacc..22fb2868e 100644 --- a/Localization/StringsConvertor/input/ar_SA/ios-infoPlist.json +++ b/Localization/StringsConvertor/input/ar_SA/ios-infoPlist.json @@ -1,6 +1,6 @@ { - "NSCameraUsageDescription": "Used to take photo for post status", - "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library", + "NSCameraUsageDescription": "يُستخدم لالتقاط الصورة عِندَ نشر الحالات", + "NSPhotoLibraryAddUsageDescription": "يُستخدم لحِفظ الصورة في مكتبة الصور", "NewPostShortcutItemTitle": "منشور جديد", "SearchShortcutItemTitle": "البحث" } diff --git a/Localization/StringsConvertor/input/ca_ES/Localizable.stringsdict b/Localization/StringsConvertor/input/ca_ES/Localizable.stringsdict index cc7312938..140185bad 100644 --- a/Localization/StringsConvertor/input/ca_ES/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ca_ES/Localizable.stringsdict @@ -21,7 +21,7 @@ a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - El límit d’entrada supera a %#@character_count@ + El límit de la entrada supera a %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -37,7 +37,7 @@ a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - El límit d’entrada continua sent %#@character_count@ + El límit de la entrada continua sent %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -111,7 +111,7 @@ one 1 impuls other - %ld impuls + %ld impulsos plural.count.vote @@ -301,9 +301,9 @@ NSStringFormatValueTypeKey ld one - fa 1a + fa 1 any other - fa %ldy anys + fa %ld anys date.month.ago.abbr @@ -317,9 +317,9 @@ NSStringFormatValueTypeKey ld one - fa 1M + fa 1 mes other - fa %ldM mesos + fa %ld mesos date.day.ago.abbr @@ -333,9 +333,9 @@ NSStringFormatValueTypeKey ld one - fa 1d + fa 1 día other - fa %ldd dies + fa %ld dies date.hour.ago.abbr @@ -351,7 +351,7 @@ one fa 1h other - fa %ldh hores + fa %ld hores date.minute.ago.abbr @@ -365,9 +365,9 @@ NSStringFormatValueTypeKey ld one - fa 1m + fa 1 minut other - fa %ldm minuts + fa %ld minuts date.second.ago.abbr @@ -381,9 +381,9 @@ NSStringFormatValueTypeKey ld one - fa 1s + fa 1 segon other - fa %lds seg + fa %ld segons diff --git a/Localization/StringsConvertor/input/de_DE/Localizable.stringsdict b/Localization/StringsConvertor/input/de_DE/Localizable.stringsdict index c868bdc0f..66b7f2a2d 100644 --- a/Localization/StringsConvertor/input/de_DE/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/de_DE/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + 1 ungelesene Benachrichtigung other - %ld unread notification + %ld ungelesene Benachrichtigungen a11y.plural.count.input_limit_exceeds diff --git a/Localization/StringsConvertor/input/de_DE/app.json b/Localization/StringsConvertor/input/de_DE/app.json index 47e57498c..43d8ed70a 100644 --- a/Localization/StringsConvertor/input/de_DE/app.json +++ b/Localization/StringsConvertor/input/de_DE/app.json @@ -536,14 +536,14 @@ } }, "account_list": { - "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", + "tab_bar_hint": "Aktuell ausgewähltes Profil: %s. Doppeltippen dann gedrückt halten, um den Kontoschalter anzuzeigen", "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "add_account": "Konto hinzufügen" }, "wizard": { - "new_in_mastodon": "New in Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "new_in_mastodon": "Neu in Mastodon", + "multiple_account_switch_intro_description": "Wechsel zwischen mehreren Konten durch drücken der Profil-Schaltfläche.", + "accessibility_hint": "Doppeltippen, um diesen Assistenten zu schließen" } } } \ No newline at end of file diff --git a/Localization/StringsConvertor/input/gd_GB/Localizable.stringsdict b/Localization/StringsConvertor/input/gd_GB/Localizable.stringsdict index 41e592a5e..7a54f553e 100644 --- a/Localization/StringsConvertor/input/gd_GB/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/gd_GB/Localizable.stringsdict @@ -13,13 +13,13 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + %ld bhrath nach deach a leughadh two - %ld unread notification + %ld bhrath nach deach a leughadh few - %ld unread notification + %ld brathan nach deach a leughadh other - %ld unread notification + %ld brath nach deach a leughadh a11y.plural.count.input_limit_exceeds diff --git a/Localization/StringsConvertor/input/gd_GB/app.json b/Localization/StringsConvertor/input/gd_GB/app.json index 35f551fea..a73925bba 100644 --- a/Localization/StringsConvertor/input/gd_GB/app.json +++ b/Localization/StringsConvertor/input/gd_GB/app.json @@ -536,14 +536,14 @@ } }, "account_list": { - "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", - "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "tab_bar_hint": "A’ phròifil air a taghadh: %s. Thoir gnogag dhùbailte is cùm sìos a ghearradh leum gu cunntas eile", + "dismiss_account_switcher": "Leig seachad taghadh a’ chunntais", + "add_account": "Cuir cunntas ris" }, "wizard": { - "new_in_mastodon": "New in Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "new_in_mastodon": "Na tha ùr ann am Mastodon", + "multiple_account_switch_intro_description": "Geàrr leum eadar iomadh cunntas le cumail sìos putan na pròifil.", + "accessibility_hint": "Thoir gnogag dhùbailte a’ leigeil seachad an draoidh seo" } } } \ No newline at end of file diff --git a/Localization/StringsConvertor/input/ja_JP/Localizable.stringsdict b/Localization/StringsConvertor/input/ja_JP/Localizable.stringsdict index 0300d9dc3..c51a9a29d 100644 --- a/Localization/StringsConvertor/input/ja_JP/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/ja_JP/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld unread notification + %ld 件の未読通知 a11y.plural.count.input_limit_exceeds @@ -27,7 +27,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 文字 a11y.plural.count.input_limit_remains @@ -41,7 +41,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 文字 plural.count.metric_formatted.post @@ -111,7 +111,7 @@ NSStringFormatValueTypeKey ld other - %ld votes + %ld票 plural.count.voter @@ -195,7 +195,7 @@ NSStringFormatValueTypeKey ld other - %ld months left + %ldか月前 date.day.left @@ -279,7 +279,7 @@ NSStringFormatValueTypeKey ld other - %ldM ago + %ld分前 date.day.ago.abbr diff --git a/Localization/StringsConvertor/input/ja_JP/app.json b/Localization/StringsConvertor/input/ja_JP/app.json index 2f1aec4ec..1c7d408f5 100644 --- a/Localization/StringsConvertor/input/ja_JP/app.json +++ b/Localization/StringsConvertor/input/ja_JP/app.json @@ -191,7 +191,7 @@ }, "scene": { "welcome": { - "slogan": "Social networking\nback in your hands." + "slogan": "ソーシャルネットワーキングを、あなたの手の中に." }, "server_picker": { "title": "サーバーを選択", @@ -538,11 +538,11 @@ "account_list": { "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "add_account": "アカウントを追加" }, "wizard": { - "new_in_mastodon": "New in Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", + "new_in_mastodon": "Mastodon の新機能", + "multiple_account_switch_intro_description": "プロフィールボタンを押して複数のアカウントを切り替えます。", "accessibility_hint": "Double tap to dismiss this wizard" } } diff --git a/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict b/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict new file mode 100644 index 000000000..064b8bf2b --- /dev/null +++ b/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict @@ -0,0 +1,390 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 agahdariya nexwendî + other + %ld agahdariyên nexwendî + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Sînorê têketinê derbas kir %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 tîp + other + %ld tîp + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Sînorê têketinê %#@character_count@ maye + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 tîp + other + %ld tîp + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + şandî + other + şandî + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 şandî + other + %ld şandî + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hezkirin + other + %ld hezkirin + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 deng + other + %ld deng + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hilbijêr + other + %ld hilbijêr + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 mirov diaxive + other + %ld mirov diaxive + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 dişopîne + other + %ld dişopîne + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 şopîner + other + %ld şopîner + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 sal berê + other + %ld sal berê + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 meh berê + other + %ld meh berê + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 roj berê + other + %ld roj berê + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 demjimêr berê + other + %ld demjimêr berê + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 çirke berê + other + %ld çirke berê + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 sal berê + other + %ld sal berê + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 roj berê + other + %ld roj berê + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 demjimêr berê + other + %ld demjimêr berê + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 çirke berê + other + %ld çirke berê + + + + diff --git a/Localization/StringsConvertor/input/kmr_TR/app.json b/Localization/StringsConvertor/input/kmr_TR/app.json new file mode 100644 index 000000000..9798c86c2 --- /dev/null +++ b/Localization/StringsConvertor/input/kmr_TR/app.json @@ -0,0 +1,549 @@ +{ + "common": { + "alerts": { + "common": { + "please_try_again": "Ji kerema xwe dîsa biceribîne.", + "please_try_again_later": "Ji kerema xwe paşê dîsa biceribîne." + }, + "sign_up_failure": { + "title": "Tomarkirin têkçû" + }, + "server_error": { + "title": "Çewtiya rajekar" + }, + "vote_failure": { + "title": "Dengdayîn têkçû", + "poll_ended": "Rapirsîya qediya" + }, + "discard_post_content": { + "title": "Reşnivîs jêbibe", + "message": "Piştrast bikin ku naveroka posteyê ya hatîye nivîsandin jê bibin." + }, + "publish_post_failure": { + "title": "Weşandin têkçû", + "message": "Weşandina şandiyê têkçû.\nJkx girêdana înternetê xwe kontrol bike.", + "attachments_message": { + "video_attach_with_photo": "Nikare vîdyoyekê tevlî şandiyê ku berê wêne tê de heye bike.", + "more_than_one_video": "Nikare ji bêtirî yek vîdyoyekê tevlî şandiyê bike." + } + }, + "edit_profile_failure": { + "title": "Çewtiya profîlê biguherîne", + "message": "Nikare profîlê serrast bike. Jkx dîsa biceribîne." + }, + "sign_out": { + "title": "Derkeve", + "message": "Ma tu dixwazî ku derkevî?", + "confirm": "Derkeve" + }, + "block_domain": { + "title": "Tu ji xwe bawerî, bi rastî tu dixwazî hemû %s asteng bikî? Di gelek rewşan de asteng kirin an jî bêdeng kirin têrê dike û tê tercîh kirin. Tu nikarî naveroka vê navperê di demnameyê an jî agahdariyên xwe de bibînî. Şopînerên te yê di vê navperê were jêbirin.", + "block_entire_domain": "Navperê asteng bike" + }, + "save_photo_failure": { + "title": "Tomarkirina wêneyê têkçû", + "message": "Ji kerema xwe destûra gihîştina pirtûkxaneya wêneyê çalak bikin da ku wêneyê hilînin." + }, + "delete_post": { + "title": "Ma tu dixwazî vê şandiyê jê bibî?", + "delete": "Jê bibe" + }, + "clean_cache": { + "title": "Pêşbîrê paqij bike", + "message": "Pêşbîra %s biserketî hate paqijkirin." + } + }, + "controls": { + "actions": { + "back": "Vegere", + "next": "Pêş", + "previous": "Paş", + "open": "Veke", + "add": "Tevlî bike", + "remove": "Rake", + "edit": "Serrast bike", + "save": "Tomar bike", + "ok": "BAŞ E", + "done": "Qediya", + "confirm": "Bipejirîne", + "continue": "Bidomîne", + "cancel": "Dev jê berde", + "discard": "Biavêje", + "try_again": "Dîsa biceribîne", + "take_photo": "Wêne bikişîne", + "save_photo": "Wêneyê hilîne", + "copy_photo": "Wêne kopî bikin", + "sign_in": "Têkeve", + "sign_up": "Tomar bibe", + "see_more": "Bêtir bibîne", + "preview": "Pêşdîtin", + "share": "Parve bike", + "share_user": "%s parve bike", + "share_post": "Şandiyê parve bike", + "open_in_safari": "Di Safariyê de veke", + "find_people": "Kesên ku bişopînin bibînin", + "manually_search": "Ji devlê i destan lêgerînê bike", + "skip": "Derbas bike", + "reply": "Bersivê bide", + "report_user": "%s ragihîne", + "block_domain": "%s asteng bike", + "unblock_domain": "%s asteng neke", + "settings": "Sazkarî", + "delete": "Jê bibe" + }, + "tabs": { + "home": "Serrûpel", + "search": "Bigere", + "notification": "Agahdarî", + "profile": "Profîl" + }, + "keyboard": { + "common": { + "switch_to_tab": "Biguherîne bo %s", + "compose_new_post": "Şandiyeke nû binivsîne", + "show_favorites": "Bijarteyan nîşan bide", + "open_settings": "Sazkariyan Veke" + }, + "timeline": { + "previous_status": "Şandeya paş", + "next_status": "Şandiya pêş", + "open_status": "Şandiyê veke", + "open_author_profile": "Profîla nivîskaran veke", + "open_reblogger_profile": "Profîla nivîskaran veke", + "reply_status": "Bersivê bide şandiyê", + "toggle_reblog": "Toggle Reblog on Post", + "toggle_favorite": "Di postê da Bijartin veke/bigire", + "toggle_content_warning": "Hişyariya naverokê veke/bigire", + "preview_image": "Wêneya pêşdîtinê" + }, + "segmented_control": { + "previous_section": "Beşa berê", + "next_section": "Beşa paşê" + } + }, + "status": { + "user_reblogged": "%s ji nû ve hat blogkirin", + "user_replied_to": "Bersiv da %s", + "show_post": "Şandiyê nîşan bide", + "show_user_profile": "Profîla bikarhêner nîşan bide", + "content_warning": "Hişyariya naverokê", + "media_content_warning": "Ji bo aşkerakirinê derekî bitikîne", + "poll": { + "vote": "Deng", + "closed": "Girtî" + }, + "actions": { + "reply": "Bersivê bide", + "reblog": "Ji nû ve blog", + "unreblog": "Ji nû ve blogkirin betal bikin", + "favorite": "Bijartî", + "unfavorite": "Nebijare", + "menu": "Menû" + }, + "tag": { + "url": "URL", + "mention": "Behs", + "link": "Girêdan", + "hashtag": "Etîket", + "email": "E-name", + "emoji": "E-name" + } + }, + "friendship": { + "follow": "Bişopîne", + "following": "Dişopîne", + "request": "Daxwazên şopandinê", + "pending": "Tê nirxandin", + "block": "Asteng bike", + "block_user": "%s asteng bike", + "block_domain": "%s asteng bike", + "unblock": "Astengiyê rake", + "unblock_user": "%s asteng neke", + "blocked": "Astengkirî", + "mute": "Bêdeng bike", + "mute_user": "%s bêdeng bike", + "unmute": "Bêdeng neke", + "unmute_user": "%s bêdeng neke", + "muted": "Bêdengkirî", + "edit_info": "Zanyariyan serrast bike" + }, + "timeline": { + "filtered": "Parzûnkirî", + "timestamp": { + "now": "Niha" + }, + "loader": { + "load_missing_posts": "Barkirina posteyên kêm", + "loading_missing_posts": "Barkirina posteyên kêm...", + "show_more_replies": "Bêtir bersivan nîşan bide" + }, + "header": { + "no_status_found": "Şandî nehate dîtin", + "blocking_warning": "Tu nikarî profîla vî bikarhênerî bibînî\nHeta ku tu wan asteng bikî.\nProfîla te ji wan ra wiha xuya dike.", + "user_blocking_warning": "Tu nikarî profîla %s bibînî\nHeta ku tu wan asteng bikî.\nProfîla te ji wan ra wiha xuya dike.", + "blocked_warning": "Tu nikarî profîla vî bikarhênerî bibînî\nheta ku astengîya te rakin.", + "user_blocked_warning": "Tu nikarî profîla %s bibînî\nHeta ku astengîya te rakin.", + "suspended_warning": "Ev bikarhêner hat sekinandin.", + "user_suspended_warning": "Hesaba %s hat sekinandin." + } + } + } + }, + "scene": { + "welcome": { + "slogan": "Torên civakî\ndi destên te de." + }, + "server_picker": { + "title": "Rajekarekê hilbijêre,\nHer kîjan rajekar be.", + "button": { + "category": { + "all": "Hemû", + "all_accessiblity_description": "Beş: Hemû", + "academia": "akademî", + "activism": "çalakî", + "food": "xwarin", + "furry": "furry", + "games": "lîsk", + "general": "giştî", + "journalism": "rojnamevanî", + "lgbt": "lgbt", + "regional": "herêmî", + "art": "huner", + "music": "muzîk", + "tech": "teknolojî" + }, + "see_less": "Kêmtir bibîne", + "see_more": "Bêtir bibîne" + }, + "label": { + "language": "ZIMAN", + "users": "BIKARHÊNER", + "category": "KATEGORÎ" + }, + "input": { + "placeholder": "Serverek bibînin an jî beşdarî ya xwe bibin..." + }, + "empty_state": { + "finding_servers": "Dîtina serverên berdest...", + "bad_network": "Di dema barkirina daneyan da tiştek xelet derket. Girêdana xwe ya înternetê kontrol bike.", + "no_results": "Encam nade" + } + }, + "register": { + "title": "Ji me re hinekî qala xwe bike.", + "input": { + "avatar": { + "delete": "Jê bibe" + }, + "username": { + "placeholder": "navê bikarhêner", + "duplicate_prompt": "Navê vê bikarhêner tê girtin." + }, + "display_name": { + "placeholder": "navê nîşanê" + }, + "email": { + "placeholder": "e-name" + }, + "password": { + "placeholder": "şîfre", + "hint": "Şîfreya we herî kêm heşt tîpan hewce dike" + }, + "invite": { + "registration_user_invite_request": "Tu çima dixwazî beşdar bibî?" + } + }, + "error": { + "item": { + "username": "Navê bikarhêner", + "email": "E-name", + "password": "Şîfre", + "agreement": "Lihevhatin", + "locale": "Herêm", + "reason": "Sedem" + }, + "reason": { + "blocked": "%s peydekerê e-nameya bêdestûr dihewîne", + "unreachable": "%s xuya nake", + "taken": "%s jixwe tê bikaranîn", + "reserved": "%s peyveke mifteya veqetandî ye", + "accepted": "%s divê were qebûlkirin", + "blank": "%s pêwist e", + "invalid": "%s ne derbasdar e", + "too_long": "%s gelekî dirêj e", + "too_short": "%s pir kurt e", + "inclusion": "%s nirxeke ku tê destekirin nîn e" + }, + "special": { + "username_invalid": "Navê bikarhêner divê tenê tîpên alfanumerîk û binxet hebe", + "username_too_long": "Navê bikarhêner pir dirêj e (ji 30 tîpan dirêjtir nabe)", + "email_invalid": "Ev ne navnîşana e-nameyek derbasdar e", + "password_too_short": "Şîfre pir kurt e (divê herî kêm 8 tîpan be)" + } + } + }, + "server_rules": { + "title": "Hin qaîdeyên bingehîn.", + "subtitle": "Ev rêzik ji aliyê rêvebirên %s ve tên sazkirin.", + "prompt": "Bi berdewamî, hûn ji bo %s di bin şertên polîtîkaya xizmet û nepenîtiyê da ne.", + "terms_of_service": "şert û mercên xizmetê", + "privacy_policy": "polîtîkaya nepenîtiyê", + "button": { + "confirm": "Ez tev dibim" + } + }, + "confirm_email": { + "title": "Tiştekî dawî.", + "subtitle": "We just sent an email to %s,\ntap the link to confirm your account.", + "button": { + "open_email_app": "Sepana e-nameyê veke", + "dont_receive_email": "Min hîç e-nameyeke nesitand" + }, + "dont_receive_email": { + "title": "E-nameyê xwe kontrol bike", + "description": "Kontrol bike ka navnîşana e-nameya te rast e û her wiha peldanka xwe ya spam.", + "resend_email": "E-namyê yê dîsa bişîne" + }, + "open_email_app": { + "title": "Check your inbox.", + "description": "We just sent you an email. Check your junk folder if you haven’t.", + "mail": "E-name", + "open_email_client": "Rajegirê e-nameyê veke" + } + }, + "home_timeline": { + "title": "Serrûpel", + "navigation_bar_state": { + "offline": "Derhêl", + "new_posts": "Şandiyên nû bibîne", + "published": "Hate weşandin!", + "Publishing": "Şandî tê weşandin..." + } + }, + "suggestion_account": { + "title": "Kesên bo ku bişopînî bibîne", + "follow_explain": "Gava tu kesekî dişopînî, tu yê şandiyê wan di serrûpelê de bibîne." + }, + "compose": { + "title": { + "new_post": "Şandiya nû", + "new_reply": "Bersiva nû" + }, + "media_selection": { + "camera": "Wêne bikişîne", + "photo_library": "Wênegeh", + "browse": "Bigere" + }, + "content_input_placeholder": "Type or paste what’s on your mind", + "compose_action": "Biweşîne", + "replying_to_user": "bersiv bide %s", + "attachment": { + "photo": "wêne", + "video": "vîdyo", + "attachment_broken": "Ev %s naxebite û nayê barkirin\n li ser Mastodon.", + "description_photo": "Describe the photo for the visually-impaired...", + "description_video": "Describe the video for the visually-impaired..." + }, + "poll": { + "duration_time": "Dirêjî: %s", + "thirty_minutes": "30 xulek", + "one_hour": "1 Demjimêr", + "six_hours": "6 Demjimêr", + "one_day": "1 Roj", + "three_days": "3 Roj", + "seven_days": "7 Roj", + "option_number": "Vebijêrk %ld" + }, + "content_warning": { + "placeholder": "Write an accurate warning here..." + }, + "visibility": { + "public": "Gelemperî", + "unlisted": "Nerêzokkirî", + "private": "Tenê şopîneran", + "direct": "Tenê mirovên ku min qalkirî" + }, + "auto_complete": { + "space_to_add": "Space to add" + }, + "accessibility": { + "append_attachment": "Pêvek tevlî bike", + "append_poll": "Rapirsî tevlî bike", + "remove_poll": "Rapirsî rake", + "custom_emoji_picker": "Custom Emoji Picker", + "enable_content_warning": "Enable Content Warning", + "disable_content_warning": "Hişyariya naverokê neçalak bike", + "post_visibility_menu": "Menuya Xuyabûna Şandiyê" + }, + "keyboard": { + "discard_post": "Şandî bihelîne", + "publish_post": "Şandiye bide weşan", + "toggle_poll": "Anketê veke/bigire", + "toggle_content_warning": "Hişyariya naverokê veke/bigire", + "append_attachment_entry": "Pêvek lê zêde bike - %s", + "select_visibility_entry": "Xuyanîbûn hilbijêre - %s" + } + }, + "profile": { + "dashboard": { + "posts": "şandîyan", + "following": "dişopîne", + "followers": "şopîneran" + }, + "fields": { + "add_row": "Rêzê lê zêde bike", + "placeholder": { + "label": "Nîşan", + "content": "Naverok" + } + }, + "segmented_control": { + "posts": "Şandîyan", + "replies": "Bersivan", + "media": "Medya" + }, + "relationship_action_alert": { + "confirm_unmute_user": { + "title": "Hesabê ji bê deng rake", + "message": "Ji bo vekirina bê dengkirinê bipejirin %s" + }, + "confirm_unblock_usre": { + "title": "Hesabê ji bloke rake", + "message": "Ji bo rakirina blokê bipejirin %s" + } + } + }, + "search": { + "title": "Bigere", + "search_bar": { + "placeholder": "Li etîketan û bikarhêneran bigerin", + "cancel": "Betal kirin" + }, + "recommend": { + "button_text": "Hemûyé bibîne", + "hash_tag": { + "title": "Trend li ser Mastodon", + "description": "Etîketên ku pir balê dikişînin", + "people_talking": "%s kes diaxivin" + }, + "accounts": { + "title": "Hesabên ku hûn dikarin hez bikin", + "description": "Dibe ku tu bixwazî van hesaban bişopînî", + "follow": "Bişopîne" + } + }, + "searching": { + "segment": { + "all": "Hemû", + "people": "Mirov", + "hashtags": "Etîketan", + "posts": "Şandîyan" + }, + "empty_state": { + "no_results": "Encam tune" + }, + "recent_search": "Lêgerînên dawî", + "clear": "Paqij bike" + } + }, + "favorite": { + "title": "Bijareyên te" + }, + "notification": { + "title": { + "Everything": "Her tişt", + "Mentions": "Behs" + }, + "user_followed_you": "%s te şopand", + "user_favorited your post": "%s posta we bijarte", + "user_reblogged_your_post": "%s posta we ji nû ve tomar kir", + "user_mentioned_you": "%s behsa te kir", + "user_requested_to_follow_you": "%s daxwaza şopandina te kir", + "user_your_poll_has_ended": "%s Anketa te qediya", + "keyobard": { + "show_everything": "Her tiştî nîşan bide", + "show_mentions": "Behskirîya nîşan bike" + } + }, + "thread": { + "back_title": "Şandî", + "title": "Post from %s" + }, + "settings": { + "title": "Sazkarî", + "section": { + "appearance": { + "title": "Xuyang", + "automatic": "Xweber", + "light": "Her dem ronî", + "dark": "Her dem tarî" + }, + "notifications": { + "title": "Agahdarî", + "favorites": "Şandiyên min hez kir", + "follows": "Min şopand", + "boosts": "Reblogs my post", + "mentions": "Qale min kir", + "trigger": { + "anyone": "her kes", + "follower": "şopînerek", + "follow": "her kesê ku dişopînim", + "noone": "ne yek", + "title": "Min agahdar bike gava" + } + }, + "preference": { + "title": "Hilbijarte", + "true_black_dark_mode": "True black dark mode", + "disable_avatar_animation": "Disable animated avatars", + "disable_emoji_animation": "Disable animated emojis", + "using_default_browser": "Use default browser to open links" + }, + "boring_zone": { + "title": "The Boring Zone", + "account_settings": "Account Settings", + "terms": "Terms of Service", + "privacy": "Privacy Policy" + }, + "spicy_zone": { + "title": "The Spicy Zone", + "clear": "Clear Media Cache", + "signout": "Sign Out" + } + }, + "footer": { + "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)" + }, + "keyboard": { + "close_settings_window": "Close Settings Window" + } + }, + "report": { + "title": "%s ragihîne", + "step1": "Gav 1 ji 2", + "step2": "Gav 2 ji 2", + "content1": "Are there any other posts you’d like to add to the report?", + "content2": "Is there anything the moderators should know about this report?", + "send": "Ragihandinê bişîne", + "skip_to_send": "Bêyî şirove bişîne", + "text_placeholder": "Type or paste additional comments" + }, + "preview": { + "keyboard": { + "close_preview": "Pêşdîtin bigire", + "show_next": "A pêş nîşan bide", + "show_previous": "A paş nîşan bide" + } + }, + "account_list": { + "tab_bar_hint": "Profîla hilbijartî ya niha: %s. Du caran bitikîne û paşê dest bide ser da ku guhêrbara ajimêr were nîşandan", + "dismiss_account_switcher": "Dismiss Account Switcher", + "add_account": "Ajimêr tevlî bike" + }, + "wizard": { + "new_in_mastodon": "Nû di Mastodon de", + "multiple_account_switch_intro_description": "Dest bide ser bişkoja profîlê da ku di navbera gelek ajimêrann de biguherînî.", + "accessibility_hint": "Double tap to dismiss this wizard" + } + } +} \ No newline at end of file diff --git a/Localization/StringsConvertor/input/kmr_TR/ios-infoPlist.json b/Localization/StringsConvertor/input/kmr_TR/ios-infoPlist.json new file mode 100644 index 000000000..cdb286c00 --- /dev/null +++ b/Localization/StringsConvertor/input/kmr_TR/ios-infoPlist.json @@ -0,0 +1,6 @@ +{ + "NSCameraUsageDescription": "Bo kişandina wêneyê ji bo rewşa şandiyan tê bikaranîn", + "NSPhotoLibraryAddUsageDescription": "Ji bo tomarkirina wêneyê di pirtûkxaneya wêneyan de tê bikaranîn", + "NewPostShortcutItemTitle": "Şandiya nû", + "SearchShortcutItemTitle": "Bigere" +} diff --git a/Localization/StringsConvertor/input/th_TH/Localizable.stringsdict b/Localization/StringsConvertor/input/th_TH/Localizable.stringsdict index 1d6ff10bc..8971821f6 100644 --- a/Localization/StringsConvertor/input/th_TH/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/th_TH/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld unread notification + %ld การแจ้งเตือนที่ยังไม่ได้อ่าน a11y.plural.count.input_limit_exceeds diff --git a/Localization/StringsConvertor/input/th_TH/app.json b/Localization/StringsConvertor/input/th_TH/app.json index 707add6f7..fb3024f2b 100644 --- a/Localization/StringsConvertor/input/th_TH/app.json +++ b/Localization/StringsConvertor/input/th_TH/app.json @@ -536,14 +536,14 @@ } }, "account_list": { - "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher", - "dismiss_account_switcher": "Dismiss Account Switcher", - "add_account": "Add Account" + "tab_bar_hint": "โปรไฟล์ที่เลือกในปัจจุบัน: %s แตะสองครั้งแล้วกดค้างไว้เพื่อแสดงตัวสลับบัญชี", + "dismiss_account_switcher": "ปิดตัวสลับบัญชี", + "add_account": "เพิ่มบัญชี" }, "wizard": { - "new_in_mastodon": "New in Mastodon", - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", - "accessibility_hint": "Double tap to dismiss this wizard" + "new_in_mastodon": "มาใหม่ใน Mastodon", + "multiple_account_switch_intro_description": "สลับระหว่างหลายบัญชีโดยกดปุ่มโปรไฟล์ค้างไว้", + "accessibility_hint": "แตะสองครั้งเพื่อปิดตัวช่วยสร้างนี้" } } } \ No newline at end of file diff --git a/Localization/app.json b/Localization/app.json index 3ec77cf10..5c01ae7e0 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -67,6 +67,7 @@ "done": "Done", "confirm": "Confirm", "continue": "Continue", + "compose": "Compose", "cancel": "Cancel", "discard": "Discard", "try_again": "Try Again", @@ -413,6 +414,12 @@ } } }, + "follower": { + "footer": "Followers from other servers are not displayed." + }, + "following": { + "footer": "Follows from other servers are not displayed." + }, "search": { "title": "Search", "search_bar": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 07a833da7..60f0f5d74 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -187,6 +187,8 @@ DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; + DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; + DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; }; @@ -331,6 +333,17 @@ DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A06225E905E000CFDF14 /* UIApplication.swift */; }; DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; }; DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; }; + DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; }; + DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */; }; + DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */; }; + DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */; }; + DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */; }; + DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */; }; + DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FB272FF55800C70B6E /* UserSection.swift */; }; + DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FD272FF59000C70B6E /* UserItem.swift */; }; + DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; }; + DB6B75022730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */; }; + DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; }; DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; @@ -347,6 +360,8 @@ DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; + DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CA271D5A0300BE3819 /* LineChartView.swift */; }; + DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; }; DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; }; @@ -356,6 +371,13 @@ DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73B48F261F030A002E9E9F /* SafariActivity.swift */; }; + DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */; }; + DB73BF4127118B6D00781945 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4027118B6D00781945 /* Instance.swift */; }; + DB73BF43271192BB00781945 /* InstanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF42271192BB00781945 /* InstanceService.swift */; }; + DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */; }; + DB73BF47271199CA00781945 /* Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF46271199CA00781945 /* Instance.swift */; }; + DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */; }; + DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */; }; DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; }; DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; }; DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; }; @@ -454,7 +476,6 @@ DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; }; DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6484267D0F9E007FE9FD /* StatusNode.swift */; }; DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6487267D388B007FE9FD /* ASTableNode.swift */; }; - DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */; }; DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */; }; DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */; }; DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; }; @@ -950,6 +971,8 @@ DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; + DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = ""; }; + DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; @@ -1120,6 +1143,17 @@ DB68A06225E905E000CFDF14 /* UIApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; }; DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = ""; }; + DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = ""; }; + DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewModel.swift; sourceTree = ""; }; + DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+Diffable.swift"; sourceTree = ""; }; + DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewModel+State.swift"; sourceTree = ""; }; + DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowerListViewController+Provider.swift"; sourceTree = ""; }; + DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follower.swift"; sourceTree = ""; }; + DB6B74FB272FF55800C70B6E /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; + DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; + DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = ""; }; + DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProviderFacade+UITableViewDelegate.swift"; sourceTree = ""; }; + DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = ""; }; DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePreference.swift; sourceTree = ""; }; DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = ""; }; @@ -1135,6 +1169,8 @@ DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = ""; }; DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = ""; }; + DB71C7CA271D5A0300BE3819 /* LineChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartView.swift; sourceTree = ""; }; + DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurveAlgorithm.swift; sourceTree = ""; }; DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = ""; }; DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; }; @@ -1144,6 +1180,13 @@ DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB73B48F261F030A002E9E9F /* SafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariActivity.swift; sourceTree = ""; }; + DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Notification.swift"; sourceTree = ""; }; + DB73BF4027118B6D00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + DB73BF42271192BB00781945 /* InstanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceService.swift; sourceTree = ""; }; + DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Instance.swift"; sourceTree = ""; }; + DB73BF46271199CA00781945 /* Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instance.swift; sourceTree = ""; }; + DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewDiffableDataSource.swift; sourceTree = ""; }; + DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewDiffableDataSource.swift; sourceTree = ""; }; DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = ""; }; DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; @@ -1270,7 +1313,6 @@ DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = ""; }; DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = ""; }; - DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = ""; }; DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = ""; }; DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = ""; }; DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = ""; }; @@ -1345,6 +1387,11 @@ DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreference.swift; sourceTree = ""; }; DBD376B1269302A4007FEC24 /* UITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = ""; }; DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = ""; }; + DBDC1CF9272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/Intents.strings"; sourceTree = ""; }; + DBDC1CFA272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ku-TR"; path = "ku-TR.lproj/Localizable.stringsdict"; sourceTree = ""; }; + DBDC1CFB272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/Localizable.strings"; sourceTree = ""; }; + DBDC1CFC272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ku-TR"; path = "ku-TR.lproj/InfoPlist.strings"; sourceTree = ""; }; + DBDC1CFD272C0FD600055C3D /* ku-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ku-TR"; path = "ku-TR.lproj/Intents.stringsdict"; sourceTree = ""; }; DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = ""; }; DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = ""; }; DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineHeaderTableViewCell.swift; sourceTree = ""; }; @@ -1738,6 +1785,7 @@ 2D5A3D0125CF8640002347D6 /* Vender */ = { isa = PBXGroup; children = ( + DB71C7CC271D7F4300BE3819 /* CurveAlgorithm.swift */, 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB51D170262832380062B7A1 /* BlurHashDecode.swift */, DB51D171262832380062B7A1 /* BlurHashEncode.swift */, @@ -1771,6 +1819,7 @@ DB297B1A2679FAE200704C90 /* PlaceholderImageCacheService.swift */, DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */, DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */, + DB73BF42271192BB00781945 /* InstanceService.swift */, ); path = Service; sourceTree = ""; @@ -1839,6 +1888,7 @@ 2D4AD8A126316CD200613EFC /* SelectedAccountSection.swift */, DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */, + DB6B74FB272FF55800C70B6E /* UserSection.swift */, ); path = Section; sourceTree = ""; @@ -1882,8 +1932,10 @@ 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */, 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */, DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */, + DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */, DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */, DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */, + DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */, ); path = TableviewCell; sourceTree = ""; @@ -1892,6 +1944,7 @@ isa = PBXGroup; children = ( 2D7631B225C159F700929FB9 /* Item.swift */, + DB6B74FD272FF59000C70B6E /* UserItem.swift */, 2D198642261BF09500F0B013 /* SearchResultItem.swift */, DB4F097C26A03A5B00D62E92 /* SearchHistoryItem.swift */, 2D4AD8A726316D3500613EFC /* SelectedAccountItem.swift */, @@ -1941,6 +1994,7 @@ isa = PBXGroup; children = ( 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */, + DB71C7CA271D5A0300BE3819 /* LineChartView.swift */, ); path = View; sourceTree = ""; @@ -2079,6 +2133,7 @@ DBAFB7342645463500371D5F /* Emojis.swift */, DBA94439265CC0FC00C537E1 /* Fields.swift */, DBA1DB7F268F84F80052DB59 /* NotificationType.swift */, + DB73BF46271199CA00781945 /* Instance.swift */, ); path = CoreDataStack; sourceTree = ""; @@ -2105,6 +2160,7 @@ DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */, DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */, DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */, + DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */, ); path = View; sourceTree = ""; @@ -2259,6 +2315,7 @@ 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */, 2D34D9DA261494120081BFC0 /* APIService+Search.swift */, 0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */, + DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */, DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */, 5B24BBE1262DB19100A9381B /* APIService+Report.swift */, DBAE3F932616E28B004B8251 /* APIService+Follow.swift */, @@ -2280,6 +2337,7 @@ 2D79E700261EA5550011E398 /* APIService+CoreData+Tag.swift */, DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */, 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */, + DB73BF44271195AC00781945 /* APIService+CoreData+Instance.swift */, ); path = CoreData; sourceTree = ""; @@ -2454,6 +2512,7 @@ DB6804912637CD8700430867 /* AppName.swift */, DB6804FC2637CFEC00430867 /* AppSecret.swift */, DB6804D02637CE4700430867 /* UserDefaults.swift */, + DB73BF3A2711885500781945 /* UserDefaults+Notification.swift */, ); path = AppShared; sourceTree = ""; @@ -2476,6 +2535,18 @@ path = NavigationController; sourceTree = ""; }; + DB6B74F0272FB55400C70B6E /* Follower */ = { + isa = PBXGroup; + children = ( + DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */, + DB6B74F7272FBFB100C70B6E /* FollowerListViewController+Provider.swift */, + DB6B74F1272FB67600C70B6E /* FollowerListViewModel.swift */, + DB6B74F3272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift */, + DB6B74F5272FBCDB00C70B6E /* FollowerListViewModel+State.swift */, + ); + path = Follower; + sourceTree = ""; + }; DB6C8C0525F0921200AAA452 /* MastodonSDK */ = { isa = PBXGroup; children = ( @@ -2565,6 +2636,7 @@ isa = PBXGroup; children = ( DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */, + DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */, DB852D1A26FAED0100FC9D81 /* Sidebar */, DB8AF54E25C13703002E6C99 /* MainTab */, ); @@ -2636,6 +2708,7 @@ 5B90C46D26259B2C0002E742 /* Setting.swift */, 5B90C46C26259B2C0002E742 /* Subscription.swift */, 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */, + DB73BF4027118B6D00781945 /* Instance.swift */, ); path = Entity; sourceTree = ""; @@ -2716,7 +2789,6 @@ DB0E91E926A9675100BD2ACC /* MetaLabel.swift */, DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, - DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */, DB0140CE25C42AEE00F9F3CF /* OSLog.swift */, 2D939AB425EDD8A90076FA61 /* String.swift */, DB68A06225E905E000CFDF14 /* UIApplication.swift */, @@ -2736,6 +2808,8 @@ DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */, DBCC3B2F261440A50045B23D /* UITabBarController.swift */, DBCC3B35261440BA0045B23D /* UINavigationController.swift */, + DB73BF4827140BA300781945 /* UICollectionViewDiffableDataSource.swift */, + DB73BF4A27140C0800781945 /* UITableViewDiffableDataSource.swift */, ); path = Extension; sourceTree = ""; @@ -2827,6 +2901,7 @@ DBB525462611ED57002F1F29 /* Header */, DBB5253B2611ECF5002F1F29 /* Timeline */, DBE3CDF1261C6B3100430CC6 /* Favorite */, + DB6B74F0272FB55400C70B6E /* Follower */, DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */, DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */, DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */, @@ -2947,6 +3022,7 @@ children = ( DBAE3F672615DD60004B8251 /* UserProvider.swift */, DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */, + DB6B75012730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift */, ); path = UserProvider; sourceTree = ""; @@ -3500,6 +3576,7 @@ ru, "gd-GB", th, + "ku-TR", ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( @@ -3849,6 +3926,7 @@ DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, + DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, @@ -3911,6 +3989,7 @@ DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */, 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, + DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */, DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */, @@ -3927,6 +4006,7 @@ DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */, 0FAA102725E1126A0017CCDE /* MastodonPickServerViewController.swift in Sources */, DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, + DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, DBE3CE07261D6A0E00430CC6 /* FavoriteViewModel+Diffable.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */, @@ -3943,7 +4023,6 @@ DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */, DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */, - DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */, DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, @@ -3993,6 +4072,7 @@ DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */, 2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */, + DB73BF47271199CA00781945 /* Instance.swift in Sources */, DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */, @@ -4002,6 +4082,7 @@ DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */, + DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */, @@ -4015,6 +4096,7 @@ 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */, 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, + DB73BF43271192BB00781945 /* InstanceService.swift in Sources */, DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */, 2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */, DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */, @@ -4054,6 +4136,9 @@ DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */, 5D0393902612D259007FE196 /* WebViewController.swift in Sources */, DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */, + DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */, + DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */, + DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */, DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */, 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */, DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, @@ -4066,6 +4151,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, + DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, @@ -4082,6 +4168,7 @@ DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */, DB023295267F0AB800031745 /* ASMetaEditableTextNode.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, + DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */, DB4F096C269EFA2000D62E92 /* SearchResultViewController+StatusProvider.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */, @@ -4097,7 +4184,9 @@ DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */, DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */, + DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */, DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */, + DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */, 5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */, DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */, DBBC24C426A544B900398BB9 /* Theme.swift in Sources */, @@ -4139,8 +4228,10 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, + DB6B75022730060700C70B6E /* UserProviderFacade+UITableViewDelegate.swift in Sources */, DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, 2D34D9CB261489930081BFC0 /* SearchViewController+Recommend.swift in Sources */, + DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */, DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */, DB482A45261335BA008AE74C /* UserTimelineViewController+Provider.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, @@ -4190,6 +4281,7 @@ DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, 5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */, + DB73BF45271195AC00781945 /* APIService+CoreData+Instance.swift in Sources */, DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */, DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, @@ -4202,12 +4294,14 @@ DBE54AC62636C89F004E7C0B /* NotificationPreference.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, + DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */, DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */, 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, + DB71C7CD271D7F4300BE3819 /* CurveAlgorithm.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */, @@ -4217,6 +4311,7 @@ 0F202227261411BB000C64BF /* HashtagTimelineViewController+Provider.swift in Sources */, 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, + DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */, DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */, DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */, 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, @@ -4247,6 +4342,7 @@ DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */, DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */, DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, + DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, DB4932B326F2054200EF46D4 /* CircleAvatarButton.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, @@ -4304,6 +4400,7 @@ buildActionMask = 2147483647; files = ( DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */, + DB73BF3B2711885500781945 /* UserDefaults+Notification.swift in Sources */, DB4932B726F30F0700EF46D4 /* Array.swift in Sources */, DB6804922637CD8700430867 /* AppName.swift in Sources */, DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */, @@ -4324,6 +4421,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */, DBCC3B9B261584A00045B23D /* PrivateNote.swift in Sources */, DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */, + DB73BF4127118B6D00781945 /* Instance.swift in Sources */, DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */, DB89BA1B25C1107F008580ED /* Collection.swift in Sources */, DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */, @@ -4520,6 +4618,7 @@ DB4B777F26CA4EFA00B087B3 /* ru */, DB4B778426CA500E00B087B3 /* gd-GB */, DB4B779226CA50BA00B087B3 /* th */, + DBDC1CF9272C0FD600055C3D /* ku-TR */, ); name = Intents.intentdefinition; sourceTree = ""; @@ -4540,6 +4639,7 @@ DB4B778226CA4EFA00B087B3 /* ru */, DB4B778726CA500E00B087B3 /* gd-GB */, DB4B779526CA50BA00B087B3 /* th */, + DBDC1CFC272C0FD600055C3D /* ku-TR */, ); name = InfoPlist.strings; sourceTree = ""; @@ -4560,6 +4660,7 @@ DB4B778126CA4EFA00B087B3 /* ru */, DB4B778626CA500E00B087B3 /* gd-GB */, DB4B779426CA50BA00B087B3 /* th */, + DBDC1CFB272C0FD600055C3D /* ku-TR */, ); name = Localizable.strings; sourceTree = ""; @@ -4596,6 +4697,7 @@ DB4B778026CA4EFA00B087B3 /* ru */, DB4B778526CA500E00B087B3 /* gd-GB */, DB4B779326CA50BA00B087B3 /* th */, + DBDC1CFA272C0FD600055C3D /* ku-TR */, ); name = Localizable.stringsdict; sourceTree = ""; @@ -4616,6 +4718,7 @@ DB4B779026CA504900B087B3 /* fr */, DB4B779126CA504A00B087B3 /* ja */, DB4B779626CA50BA00B087B3 /* th */, + DBDC1CFD272C0FD600055C3D /* ku-TR */, ); name = Intents.stringsdict; sourceTree = ""; @@ -4760,7 +4863,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4789,7 +4892,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4897,11 +5000,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -4928,11 +5031,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -4957,11 +5060,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -4987,11 +5090,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5054,7 +5157,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5079,7 +5182,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5104,7 +5207,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5129,7 +5232,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5154,7 +5257,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5179,7 +5282,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5204,7 +5307,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5229,7 +5332,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5320,7 +5423,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5387,11 +5490,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5436,7 +5539,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5461,11 +5564,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5557,7 +5660,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5624,11 +5727,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = CoreDataStack/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5673,7 +5776,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5698,11 +5801,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 71; + DYLIB_CURRENT_VERSION = 82; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5728,7 +5831,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5752,7 +5855,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 068a73491..cb88c3960 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 56 + 42 CoreDataStack.xcscheme_^#shared#^_ orderHint - 54 + 43 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 51 + 44 MastodonIntents.xcscheme_^#shared#^_ @@ -117,7 +117,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 55 + 41 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1fe981b44..b305c8156 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", "state": { "branch": null, - "revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc", - "version": "5.11.1" + "revision": "a72df4849408da7e5d3c1b586797b7c601c41d1b", + "version": "5.12.1" } }, { @@ -216,15 +216,6 @@ "revision": "dad97167bf1be16aeecd109130900995dd01c515", "version": "2.6.0" } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } } ] }, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 83ae61a84..cda20255b 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -5,20 +5,24 @@ // Created by Cirno MainasuK on 2021-1-27. import UIKit +import Combine import SafariServices import CoreDataStack +import MastodonSDK import PanModal final public class SceneCoordinator { + private var disposeBag = Set() + private weak var scene: UIScene! private weak var sceneDelegate: SceneDelegate! private weak var appContext: AppContext! - private(set) weak var tabBarController: MainTabBarController! let id = UUID().uuidString - weak var splitViewController: RootSplitViewController? + private(set) weak var tabBarController: MainTabBarController! + private(set) weak var splitViewController: RootSplitViewController? private(set) var secondaryStackHashValues = Set() @@ -28,6 +32,104 @@ final public class SceneCoordinator { self.appContext = appContext scene.session.sceneCoordinator = self + + appContext.notificationService.requestRevealNotificationPublisher + .receive(on: DispatchQueue.main) + .compactMap { [weak self] pushNotification -> AnyPublisher in + guard let self = self else { return Just(nil).eraseToAnyPublisher() } + // skip if no available account + guard let currentActiveAuthenticationBox = appContext.authenticationService.activeMastodonAuthenticationBox.value else { + return Just(nil).eraseToAnyPublisher() + } + + let accessToken = pushNotification._accessToken // use raw accessToken value without normalize + if currentActiveAuthenticationBox.userAuthorization.accessToken == accessToken { + // do nothing if notification for current account + return Just(pushNotification).eraseToAnyPublisher() + } else { + // switch to notification's account + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken) + request.returnsObjectsAsFaults = false + request.fetchLimit = 1 + do { + guard let authentication = try appContext.managedObjectContext.fetch(request).first else { + return Just(nil).eraseToAnyPublisher() + } + let domain = authentication.domain + let userID = authentication.userID + return appContext.authenticationService.activeMastodonUser(domain: domain, userID: userID) + .receive(on: DispatchQueue.main) + .map { [weak self] result -> MastodonPushNotification? in + guard let self = self else { return nil } + switch result { + case .success: + // reset view hierarchy + self.setup() + return pushNotification + case .failure: + return nil + } + } + .delay(for: 1, scheduler: DispatchQueue.main) // set delay to slow transition (not must) + .eraseToAnyPublisher() + } catch { + assertionFailure(error.localizedDescription) + return Just(nil).eraseToAnyPublisher() + } + } + } + .switchToLatest() + .receive(on: DispatchQueue.main) + .sink { [weak self] pushNotification in + guard let self = self else { return } + guard let pushNotification = pushNotification else { return } + + // redirect to notification tab + self.switchToTabBar(tab: .notification) + + + // Delay in next run loop + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + // Note: + // show (push) on phone and pad + let from: UIViewController? = { + if let splitViewController = self.splitViewController { + if splitViewController.compactMainTabBarViewController.topMost?.view.window != nil { + // compact + return splitViewController.compactMainTabBarViewController.topMost + } else { + // expand + return splitViewController.contentSplitViewController.mainTabBarController.topMost + } + } else { + return self.tabBarController.topMost + } + }() + + // show notification related content + guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } + let notificationID = String(pushNotification.notificationID) + + switch type { + case .follow: + let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID) + self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) + case .followRequest: + // do nothing + break + case .mention, .reblog, .favourite, .poll, .status: + let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID) + self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) + case ._other: + assertionFailure() + break + } + } // end DispatchQueue.main.async + } + .store(in: &disposeBag) } } @@ -36,6 +138,7 @@ extension SceneCoordinator { case show // push case showDetail // replace case modal(animated: Bool, completion: (() -> Void)? = nil) + case popover(sourceView: UIView) case panModal case custom(transitioningDelegate: UIViewControllerTransitioningDelegate) case customPush @@ -75,6 +178,7 @@ extension SceneCoordinator { case accountList case profile(viewModel: ProfileViewModel) case favorite(viewModel: FavoriteViewModel) + case follower(viewModel: FollowerListViewModel) // setting case settings(viewModel: SettingsViewModel) @@ -124,6 +228,7 @@ extension SceneCoordinator { default: let splitViewController = RootSplitViewController(context: appContext, coordinator: self) self.splitViewController = splitViewController + self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController sceneDelegate.window?.rootViewController = splitViewController } } @@ -178,18 +283,7 @@ extension SceneCoordinator { switch transition { case .show: - if let splitViewController = splitViewController, !splitViewController.isCollapsed, - let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController, - (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) || - (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view)) - { - fallthrough - } else { - if secondaryStackHashValues.contains(presentingViewController.hashValue) { - secondaryStackHashValues.insert(viewController.hashValue) - } - presentingViewController.show(viewController, sender: sender) - } + presentingViewController.show(viewController, sender: sender) case .showDetail: secondaryStackHashValues.insert(viewController.hashValue) let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) @@ -214,8 +308,17 @@ extension SceneCoordinator { assertionFailure() return nil } - presentingViewController.presentPanModal(panModalPresentable) - + + // https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441 + panModalPresentable.modalPresentationStyle = .custom + panModalPresentable.modalPresentationCapturesStatusBarAppearance = true + panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default + presentingViewController.present(panModalPresentable, animated: true, completion: nil) + //presentingViewController.presentPanModal(panModalPresentable) + case .popover(let sourceView): + viewController.modalPresentationStyle = .popover + viewController.popoverPresentationController?.sourceView = sourceView + (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) case .custom(let transitioningDelegate): viewController.modalPresentationStyle = .custom viewController.transitioningDelegate = transitioningDelegate @@ -247,7 +350,13 @@ extension SceneCoordinator { } func switchToTabBar(tab: MainTabBarController.Tab) { + splitViewController?.contentSplitViewController.currentSupplementaryTab = tab + + splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue + splitViewController?.compactMainTabBarViewController.currentTab.value = tab + tabBarController.selectedIndex = tab.rawValue + tabBarController.currentTab.value = tab } } @@ -316,6 +425,10 @@ private extension SceneCoordinator { let _viewController = FavoriteViewController() _viewController.viewModel = viewModel viewController = _viewController + case .follower(let viewModel): + let _viewController = FollowerListViewController() + _viewController.viewModel = viewModel + viewController = _viewController case .suggestionAccount(let viewModel): let _viewController = SuggestionAccountViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Diffiable/Item/UserItem.swift b/Mastodon/Diffiable/Item/UserItem.swift new file mode 100644 index 000000000..6f3c591b1 --- /dev/null +++ b/Mastodon/Diffiable/Item/UserItem.swift @@ -0,0 +1,15 @@ +// +// UserItem.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import Foundation +import CoreData + +enum UserItem: Hashable { + case follower(objectID: NSManagedObjectID) + case bottomLoader + case bottomHeader(text: String) +} diff --git a/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift index 8f39eb6bd..b5c5cd8cc 100644 --- a/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift +++ b/Mastodon/Diffiable/Section/Search/SearchHistorySection.swift @@ -32,24 +32,8 @@ extension SearchHistorySection { } return cell case .status: + // Should not show status in the history list return UITableViewCell() -// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell -// if let status = try? dependency.context.managedObjectContext.existingObject(with: statusObjectID) as? Status { -// let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value -// let requestUserID = activeMastodonAuthenticationBox?.userID ?? "" -// StatusSection.configure( -// cell: cell, -// tableView: tableView, -// timelineContext: .search, -// dependency: dependency, -// readableLayoutFrame: tableView.readableContentGuide.layoutFrame, -// status: status, -// requestUserID: requestUserID, -// statusItemAttribute: attribute -// ) -// } -// cell.delegate = statusTableViewCellDelegate -// return cell } // end switch } // end UITableViewDiffableDataSource } // end func diff --git a/Mastodon/Diffiable/Section/SettingsSection.swift b/Mastodon/Diffiable/Section/SettingsSection.swift index 939fd4315..f59c13587 100644 --- a/Mastodon/Diffiable/Section/SettingsSection.swift +++ b/Mastodon/Diffiable/Section/SettingsSection.swift @@ -41,21 +41,17 @@ extension SettingsSection { switch item { case .appearance(let objectID): let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell - managedObjectContext.performAndWait { - let setting = managedObjectContext.object(with: objectID) as! Setting - cell.update(with: setting.appearance) - ManagedObjectObserver.observe(object: setting) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in - // do nothing - }, receiveValue: { [weak cell] change in - guard let cell = cell else { return } - guard case .update(let object) = change.changeType, - let setting = object as? Setting else { return } - cell.update(with: setting.appearance) - }) - .store(in: &cell.disposeBag) + UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak cell] defaults, _ in + guard let cell = cell else { return } + switch defaults.customUserInterfaceStyle { + case .unspecified: cell.update(with: .automatic) + case .dark: cell.update(with: .dark) + case .light: cell.update(with: .light) + @unknown default: + assertionFailure() + } } + .store(in: &cell.observations) cell.delegate = settingsAppearanceTableViewCellDelegate return cell case .notification(let objectID, let switchMode): diff --git a/Mastodon/Diffiable/Section/Status/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift index fe95c4c75..ceb0c9458 100644 --- a/Mastodon/Diffiable/Section/Status/StatusSection.swift +++ b/Mastodon/Diffiable/Section/Status/StatusSection.swift @@ -639,7 +639,7 @@ extension StatusSection { ) { if status.reblog != nil { cell.statusView.headerContainerView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage) + cell.statusView.headerIconLabel.configure(attributedString: StatusView.iconAttributedString(image: StatusView.reblogIconImage)) let headerText: String = { let author = status.author let name = author.displayName.isEmpty ? author.username : author.displayName @@ -657,7 +657,7 @@ extension StatusSection { cell.statusView.headerInfoLabel.isAccessibilityElement = true } else if status.inReplyToID != nil { cell.statusView.headerContainerView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerIconLabel.configure(attributedString: StatusView.iconAttributedString(image: StatusView.replyIconImage)) let headerText: String = { guard let replyTo = status.replyTo else { return L10n.Common.Controls.Status.userRepliedTo("-") @@ -720,6 +720,15 @@ extension StatusSection { statusItemAttribute: Item.StatusAttribute ) { // set content + let paragraphStyle = cell.statusView.contentMetaText.paragraphStyle + if let language = (status.reblog ?? status).language { + let direction = Locale.characterDirection(forLanguage: language) + paragraphStyle.alignment = direction == .rightToLeft ? .right : .left + } else { + paragraphStyle.alignment = .natural + } + cell.statusView.contentMetaText.paragraphStyle = paragraphStyle + if let content = content { cell.statusView.contentMetaText.configure(content: content) cell.statusView.contentMetaText.textView.accessibilityLabel = content.trimmed diff --git a/Mastodon/Diffiable/Section/UserSection.swift b/Mastodon/Diffiable/Section/UserSection.swift new file mode 100644 index 000000000..58e80c6e3 --- /dev/null +++ b/Mastodon/Diffiable/Section/UserSection.swift @@ -0,0 +1,63 @@ +// +// UserSection.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import os.log +import UIKit +import CoreData +import CoreDataStack +import MetaTextKit +import MastodonMeta + +enum UserSection: Hashable { + case main +} + +extension UserSection { + + static let logger = Logger(subsystem: "StatusSection", category: "logic") + + static func tableViewDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency, + managedObjectContext: NSManagedObjectContext + ) -> UITableViewDiffableDataSource { + UITableViewDiffableDataSource(tableView: tableView) { [ + weak dependency + ] tableView, indexPath, item -> UITableViewCell? in + guard let dependency = dependency else { return UITableViewCell() } + switch item { + case .follower(let objectID): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UserTableViewCell.self), for: indexPath) as! UserTableViewCell + managedObjectContext.performAndWait { + let user = managedObjectContext.object(with: objectID) as! MastodonUser + configure(cell: cell, user: user) + } + return cell + case .bottomLoader: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell + cell.startAnimating() + return cell + case .bottomHeader(let text): + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineFooterTableViewCell.self), for: indexPath) as! TimelineFooterTableViewCell + cell.messageLabel.text = text + return cell + } // end switch + } // end UITableViewDiffableDataSource + } // end static func tableViewDiffableDataSource { … } + +} + +extension UserSection { + + static func configure( + cell: UserTableViewCell, + user: MastodonUser + ) { + cell.configure(user: user) + } + +} diff --git a/Mastodon/Extension/CoreDataStack/Instance.swift b/Mastodon/Extension/CoreDataStack/Instance.swift new file mode 100644 index 000000000..6cacd9db9 --- /dev/null +++ b/Mastodon/Extension/CoreDataStack/Instance.swift @@ -0,0 +1,25 @@ +// +// Instance.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-9. +// + +import UIKit +import CoreDataStack +import MastodonSDK + +extension Instance { + var configuration: Mastodon.Entity.Instance.Configuration? { + guard let configurationRaw = configurationRaw else { return nil } + guard let configuration = try? JSONDecoder().decode(Mastodon.Entity.Instance.Configuration.self, from: configurationRaw) else { + return nil + } + + return configuration + } + + static func encode(configuration: Mastodon.Entity.Instance.Configuration) -> Data? { + return try? JSONEncoder().encode(configuration) + } +} diff --git a/Mastodon/Extension/CoreDataStack/Setting.swift b/Mastodon/Extension/CoreDataStack/Setting.swift index b995b80e3..4d1fc0ca5 100644 --- a/Mastodon/Extension/CoreDataStack/Setting.swift +++ b/Mastodon/Extension/CoreDataStack/Setting.swift @@ -11,9 +11,9 @@ import MastodonSDK extension Setting { - var appearance: SettingsItem.AppearanceMode { - return SettingsItem.AppearanceMode(rawValue: appearanceRaw) ?? .automatic - } +// var appearance: SettingsItem.AppearanceMode { +// return SettingsItem.AppearanceMode(rawValue: appearanceRaw) ?? .automatic +// } var activeSubscription: Subscription? { return (subscriptions ?? Set()) diff --git a/Mastodon/Extension/MetaLabel.swift b/Mastodon/Extension/MetaLabel.swift index 04b214d82..cf7d27cc0 100644 --- a/Mastodon/Extension/MetaLabel.swift +++ b/Mastodon/Extension/MetaLabel.swift @@ -111,6 +111,24 @@ extension MetaLabel { } +extension MetaLabel { + func configure(attributedString: NSAttributedString) { + let attributedString = NSMutableAttributedString(attributedString: attributedString) + + MetaText.setAttributes( + for: attributedString, + textAttributes: textAttributes, + linkAttributes: linkAttributes, + paragraphStyle: paragraphStyle, + content: PlaintextMetaContent(string: "") + ) + + textStorage.setAttributedString(attributedString) + self.attributedText = attributedString + setNeedsDisplay() + } +} + struct PlaintextMetaContent: MetaContent { let string: String let entities: [Meta.Entity] = [] diff --git a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift deleted file mode 100644 index c2ff341d9..000000000 --- a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NSDiffableDataSourceSnapshot.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-19. -// - -import UIKit - -//extension NSDiffableDataSourceSnapshot { -// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? { -// guard 0.., + completion: (() -> Void)? = nil + ) { + if #available(iOS 15.0, *) { + self.applySnapshotUsingReloadData(snapshot, completion: completion) + } else { + self.apply(snapshot, animatingDifferences: false, completion: completion) + } + } + + func applySnapshot( + _ snapshot: NSDiffableDataSourceSnapshot, + animated: Bool, + completion: (() -> Void)? = nil) { + + if #available(iOS 15.0, *) { + self.apply(snapshot, animatingDifferences: animated, completion: completion) + } else { + if animated { + self.apply(snapshot, animatingDifferences: true, completion: completion) + } else { + UIView.performWithoutAnimation { + self.apply(snapshot, animatingDifferences: true, completion: completion) + } + } + } + } +} diff --git a/Mastodon/Extension/UITableViewDiffableDataSource.swift b/Mastodon/Extension/UITableViewDiffableDataSource.swift new file mode 100644 index 000000000..5006417a4 --- /dev/null +++ b/Mastodon/Extension/UITableViewDiffableDataSource.swift @@ -0,0 +1,40 @@ +// +// UITableViewDiffableDataSource.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-11. +// + +import UIKit + +// ref: https://www.jessesquires.com/blog/2021/07/08/diffable-data-source-behavior-changes-and-reconfiguring-cells-in-ios-15/ +extension UITableViewDiffableDataSource { + func reloadData( + snapshot: NSDiffableDataSourceSnapshot, + completion: (() -> Void)? = nil + ) { + if #available(iOS 15.0, *) { + self.applySnapshotUsingReloadData(snapshot, completion: completion) + } else { + self.apply(snapshot, animatingDifferences: false, completion: completion) + } + } + + func applySnapshot( + _ snapshot: NSDiffableDataSourceSnapshot, + animated: Bool, + completion: (() -> Void)? = nil) { + + if #available(iOS 15.0, *) { + self.apply(snapshot, animatingDifferences: animated, completion: completion) + } else { + if animated { + self.apply(snapshot, animatingDifferences: true, completion: completion) + } else { + UIView.performWithoutAnimation { + self.apply(snapshot, animatingDifferences: true, completion: completion) + } + } + } + } +} diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 96fe0fca8..906dd74e2 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -96,6 +96,9 @@ internal enum Asset { internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray") } } + internal enum Sidebar { + internal static let logo = ImageAsset(name: "Scene/Sidebar/logo") + } internal enum Welcome { internal enum Illustration { internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan") @@ -160,23 +163,6 @@ internal enum Asset { internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/system/tab.bar.item.inactive.icon.color") } } - internal enum Deprecated { - internal enum Background { - internal static let danger = ColorAsset(name: "_Deprecated/Background/danger") - internal static let onboardingBackground = ColorAsset(name: "_Deprecated/Background/onboarding.background") - internal static let secondaryGroupedSystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.grouped.system.background") - internal static let secondarySystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.system.background") - internal static let systemBackground = ColorAsset(name: "_Deprecated/Background/system.background") - internal static let systemElevatedBackground = ColorAsset(name: "_Deprecated/Background/system.elevated.background") - internal static let systemGroupedBackground = ColorAsset(name: "_Deprecated/Background/system.grouped.background") - internal static let tertiarySystemBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.background") - internal static let tertiarySystemGroupedBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.grouped.background") - } - internal enum Compose { - internal static let background = ColorAsset(name: "_Deprecated/Compose/background") - internal static let toolbarBackground = ColorAsset(name: "_Deprecated/Compose/toolbar.background") - } - } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 129f6bf10..a75982e39 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 71 + 82 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -111,5 +111,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIViewControllerBasedStatusBarAppearance + diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade+UITableViewDelegate.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade+UITableViewDelegate.swift new file mode 100644 index 000000000..a6e3cf215 --- /dev/null +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade+UITableViewDelegate.swift @@ -0,0 +1,22 @@ +// +// UserProviderFacade+UITableViewDelegate.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import Combine +import CoreDataStack +import MastodonSDK +import os.log +import UIKit + +extension UserTableViewCellDelegate where Self: UserProvider { + + func handleTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath) else { return } + let user = self.mastodonUser(for: cell) + UserProviderFacade.coordinatorToUserProfileScene(provider: self, user: user) + } + +} diff --git a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift index 47490c2d8..edbe311c7 100644 --- a/Mastodon/Protocol/UserProvider/UserProviderFacade.swift +++ b/Mastodon/Protocol/UserProvider/UserProviderFacade.swift @@ -221,8 +221,6 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak sourceView, weak barButtonItem] _ in guard let provider = provider else { return } - guard let sourceView = sourceView else { return } - guard let barButtonItem = barButtonItem else { return } let activityViewController = createActivityViewControllerForMastodonUser(mastodonUser: shareUser, dependency: provider) provider.coordinator.present( scene: .activityViewController( @@ -247,8 +245,6 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak sourceView, weak barButtonItem] _ in guard let provider = provider else { return } - guard let sourceView = sourceView else { return } - guard let barButtonItem = barButtonItem else { return } let activityViewController = createActivityViewControllerForMastodonUser(status: shareStatus, dependency: provider) provider.coordinator.present( scene: .activityViewController( @@ -273,7 +269,6 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak cell] _ in guard let provider = provider else { return } - guard let cell = cell else { return } UserProviderFacade.toggleUserMuteRelationship( provider: provider, @@ -304,7 +299,6 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak cell] _ in guard let provider = provider else { return } - guard let cell = cell else { return } UserProviderFacade.toggleUserBlockRelationship( provider: provider, @@ -364,7 +358,6 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak cell] _ in guard let provider = provider else { return } - guard let cell = cell else { return } provider.context.blockDomainService.unblockDomain(userProvider: provider, cell: cell) } children.append(unblockDomainAction) @@ -378,14 +371,12 @@ extension UserProviderFacade { state: .off ) { [weak provider, weak cell] _ in guard let provider = provider else { return } - guard let cell = cell else { return } let alertController = UIAlertController(title: L10n.Common.Alerts.BlockDomain.title(mastodonUser.domainFromAcct), message: nil, preferredStyle: .alert) let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .default) { _ in } alertController.addAction(cancelAction) let blockDomainAction = UIAlertAction(title: L10n.Common.Alerts.BlockDomain.blockEntireDomain, style: .destructive) { [weak provider, weak cell] _ in guard let provider = provider else { return } - guard let cell = cell else { return } provider.context.blockDomainService.blockDomain(userProvider: provider, cell: cell) } alertController.addAction(blockDomainAction) @@ -449,3 +440,25 @@ extension UserProviderFacade { return activityViewController } } + +extension UserProviderFacade { + static func coordinatorToUserProfileScene(provider: UserProvider, user: Future) { + user + .sink { [weak provider] mastodonUser in + guard let provider = provider else { return } + guard let mastodonUser = mastodonUser else { return } + let profileViewModel = CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser) + DispatchQueue.main.async { + if provider.navigationController == nil { + let from = provider.presentingViewController ?? provider + provider.dismiss(animated: true) { + provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) + } + } else { + provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show) + } + } + } + .store(in: &provider.disposeBag) + } +} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/_Deprecated/Background/Contents.json rename to Mastodon/Resources/Assets.xcassets/Scene/Sidebar/Contents.json diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json new file mode 100644 index 000000000..4f547d09b --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf new file mode 100644 index 000000000..908727a57 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf @@ -0,0 +1,108 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 -0.103455 cm +0.168627 0.564706 0.850980 scn +27.796436 10.091343 m +33.035133 10.719734 37.596470 13.962151 38.169762 16.924883 c +39.073063 21.591980 38.998501 28.314186 38.998501 28.314186 c +38.998501 37.425270 33.056084 40.095867 33.056084 40.095867 c +30.059872 41.478233 24.914881 42.059555 19.569633 42.103455 c +19.438305 42.103455 l +14.093056 42.059555 8.951445 41.478233 5.955006 40.095867 c +5.955006 40.095867 0.012361 37.425270 0.012361 28.314186 c +0.012361 27.761837 0.009520 27.180878 0.006561 26.576080 c +-0.001656 24.896429 -0.010772 23.032921 0.037591 21.087820 c +0.253392 12.177679 1.663759 3.396290 9.864657 1.215820 c +13.645910 0.210445 16.892391 0.000000 19.507011 0.144371 c +24.248556 0.408443 26.910255 1.844212 26.910255 1.844212 c +26.753922 5.300014 l +26.753922 5.300014 23.365528 4.226753 19.560173 4.357544 c +15.789957 4.487431 11.809797 4.765984 11.200012 9.415886 c +11.143697 9.824329 11.115539 10.261055 11.115539 10.719732 c +11.115539 10.719732 14.816599 9.810978 19.507011 9.595104 c +22.375050 9.462955 25.064680 9.763912 27.796436 10.091343 c +h +31.989010 16.575367 m +31.989010 27.607372 l +31.989010 29.862061 31.417519 31.653776 30.269808 32.979347 c +29.085829 34.304916 27.535576 34.984444 25.611385 34.984444 c +23.384670 34.984444 21.698582 34.124794 20.583984 32.405266 c +19.500023 30.580288 l +18.416286 32.405266 l +17.301464 34.124794 15.615376 34.984444 13.388884 34.984444 c +11.464469 34.984444 9.914215 34.304916 8.730462 32.979347 c +7.582527 31.653776 7.011036 29.862061 7.011036 27.607372 c +7.011036 16.575367 l +11.361976 16.575367 l +11.361976 27.283108 l +11.361976 29.540287 12.307401 30.685961 14.198477 30.685961 c +16.289360 30.685961 17.337505 29.326900 17.337505 26.639557 c +17.337505 20.778585 l +21.662764 20.778585 l +21.662764 26.639557 l +21.662764 29.326900 22.710684 30.685961 24.801567 30.685961 c +26.692642 30.685961 27.638069 29.540287 27.638069 27.283108 c +27.638069 16.575367 l +31.989010 16.575367 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2035 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 39.000000 42.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002125 00000 n +0000002148 00000 n +0000002321 00000 n +0000002395 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2454 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/tab.bar.item.inactive.icon.color.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/tab.bar.item.inactive.icon.color.colorset/Contents.json index 1accfacdf..bfc2a11b2 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/tab.bar.item.inactive.icon.color.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/tab.bar.item.inactive.icon.color.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.549", - "green" : "0.510", - "red" : "0.431" + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "200", - "green" : "174", - "red" : "155" + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json index b9a69ec7d..77d24b11d 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "46", - "green" : "44", - "red" : "44" + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json index e30d6cabe..ee5b1c373 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/tab.bar.item.inactive.icon.color.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/tab.bar.item.inactive.icon.color.colorset/Contents.json index ece9000aa..bfc2a11b2 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/tab.bar.item.inactive.icon.color.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/tab.bar.item.inactive.icon.color.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.549", - "green" : "0.510", - "red" : "0.431" + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "140", - "green" : "130", - "red" : "110" + "blue" : "0x99", + "green" : "0x99", + "red" : "0x99" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json deleted file mode 100644 index dabccc33e..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.353", - "green" : "0.251", - "red" : "0.875" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json deleted file mode 100644 index 0e4687fb4..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json deleted file mode 100644 index ef6c7f7b1..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json deleted file mode 100644 index c915c8911..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.133", - "green" : "0.106", - "red" : "0.098" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json deleted file mode 100644 index 4572c2409..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json deleted file mode 100644 index 33b71ef90..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json deleted file mode 100644 index c915c8911..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.133", - "green" : "0.106", - "red" : "0.098" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json deleted file mode 100644 index 4572c2409..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json deleted file mode 100644 index 98dd7bbde..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json deleted file mode 100644 index 33b71ef90..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json deleted file mode 100644 index da7b76069..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.871", - "green" : "0.847", - "red" : "0.839" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.920", - "blue" : "0.125", - "green" : "0.125", - "red" : "0.125" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Mastodon/Resources/ar.lproj/InfoPlist.strings b/Mastodon/Resources/ar.lproj/InfoPlist.strings index 5ced1e74f..c3b26f14a 100644 --- a/Mastodon/Resources/ar.lproj/InfoPlist.strings +++ b/Mastodon/Resources/ar.lproj/InfoPlist.strings @@ -1,4 +1,4 @@ -"NSCameraUsageDescription" = "Used to take photo for post status"; -"NSPhotoLibraryAddUsageDescription" = "Used to save photo into the Photo Library"; +"NSCameraUsageDescription" = "يُستخدم لالتقاط الصورة عِندَ نشر الحالات"; +"NSPhotoLibraryAddUsageDescription" = "يُستخدم لحِفظ الصورة في مكتبة الصور"; "NewPostShortcutItemTitle" = "منشور جديد"; "SearchShortcutItemTitle" = "البحث"; \ No newline at end of file diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 3dfe057ed..5950546a9 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -1,15 +1,15 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "حظر النطاق"; "Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed."; -"Common.Alerts.CleanCache.Message" = "تم تنظيف ذاكرة التخزين المؤقت %@ بنجاح."; -"Common.Alerts.CleanCache.Title" = "تنظيف ذاكرة التخزين المؤقت"; -"Common.Alerts.Common.PleaseTryAgain" = "الرجاء المحاولة مرة أخرى."; -"Common.Alerts.Common.PleaseTryAgainLater" = "الرجاء المحاولة مرة أخرى لاحقاً."; +"Common.Alerts.CleanCache.Message" = "تمَّ مَحو ذاكرة التخزين المؤقت %@ بنجاح."; +"Common.Alerts.CleanCache.Title" = "مَحو ذاكرة التخزين المؤقت"; +"Common.Alerts.Common.PleaseTryAgain" = "يُرجى المحاولة مرة أُخرى."; +"Common.Alerts.Common.PleaseTryAgainLater" = "يُرجى المحاولة مرة أُخرى لاحقاً."; "Common.Alerts.DeletePost.Delete" = "احذف"; "Common.Alerts.DeletePost.Title" = "هل أنت متأكد من أنك تريد حذف هذا المنشور؟"; "Common.Alerts.DiscardPostContent.Message" = "Confirm to discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "تجاهل المسودة"; -"Common.Alerts.EditProfileFailure.Message" = "لا يمكن تعديل الملف الشخصي. الرجاء المحاولة مرة أخرى."; -"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error"; +"Common.Alerts.EditProfileFailure.Message" = "لا يمكن تعديل الملف الشخصي. يُرجى المحاولة مرة أُخرى."; +"Common.Alerts.EditProfileFailure.Title" = "خطأ في تَحرير الملف الشخصي"; "Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; "Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a post that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. @@ -33,7 +33,7 @@ Please check your internet connection."; "Common.Controls.Actions.CopyPhoto" = "نسخ الصورة"; "Common.Controls.Actions.Delete" = "احذف"; "Common.Controls.Actions.Discard" = "تجاهل"; -"Common.Controls.Actions.Done" = "تم"; +"Common.Controls.Actions.Done" = "تمّ"; "Common.Controls.Actions.Edit" = "تعديل"; "Common.Controls.Actions.FindPeople" = "ابحث عن أشخاص لمتابعتهم"; "Common.Controls.Actions.ManuallySearch" = "البحث يدوياً بدلاً من ذلك"; @@ -53,11 +53,11 @@ Please check your internet connection."; "Common.Controls.Actions.Share" = "شارك"; "Common.Controls.Actions.SharePost" = "شارك المنشور"; "Common.Controls.Actions.ShareUser" = "شارك %@"; -"Common.Controls.Actions.SignIn" = "لِج"; -"Common.Controls.Actions.SignUp" = "انشئ حسابًا"; +"Common.Controls.Actions.SignIn" = "تسجيل الدخول"; +"Common.Controls.Actions.SignUp" = "إنشاء حِساب"; "Common.Controls.Actions.Skip" = "تخطي"; "Common.Controls.Actions.TakePhoto" = "التقط صورة"; -"Common.Controls.Actions.TryAgain" = "حاول مرة أخرى"; +"Common.Controls.Actions.TryAgain" = "المُحاولة مرة أُخرى"; "Common.Controls.Actions.UnblockDomain" = "إلغاء حظر %@"; "Common.Controls.Friendship.Block" = "حظر"; "Common.Controls.Friendship.BlockDomain" = "حظر %@"; @@ -69,8 +69,8 @@ Please check your internet connection."; "Common.Controls.Friendship.Mute" = "أكتم"; "Common.Controls.Friendship.MuteUser" = "أكتم %@"; "Common.Controls.Friendship.Muted" = "مكتوم"; -"Common.Controls.Friendship.Pending" = "Pending"; -"Common.Controls.Friendship.Request" = "Request"; +"Common.Controls.Friendship.Pending" = "قيد المُراجعة"; +"Common.Controls.Friendship.Request" = "إرسال طَلَب"; "Common.Controls.Friendship.Unblock" = "إلغاء الحَظر"; "Common.Controls.Friendship.UnblockUser" = "إلغاء حظر %@"; "Common.Controls.Friendship.Unmute" = "إلغاء الكتم"; @@ -109,13 +109,13 @@ Please check your internet connection."; "Common.Controls.Status.Tag.Link" = "الرابط"; "Common.Controls.Status.Tag.Mention" = "أشر إلى"; "Common.Controls.Status.Tag.Url" = "عنوان URL"; -"Common.Controls.Status.UserReblogged" = "%@ reblogged"; +"Common.Controls.Status.UserReblogged" = "أعادَ %@ تدوينها"; "Common.Controls.Status.UserRepliedTo" = "رد على %@"; "Common.Controls.Tabs.Home" = "الخيط الرئيسي"; "Common.Controls.Tabs.Notification" = "الإشعارات"; "Common.Controls.Tabs.Profile" = "الملف التعريفي"; "Common.Controls.Tabs.Search" = "بحث"; -"Common.Controls.Timeline.Filtered" = "Filtered"; +"Common.Controls.Timeline.Filtered" = "مُصفَّى"; "Common.Controls.Timeline.Header.BlockedWarning" = "You can’t view this user’s profile until they unblock you."; "Common.Controls.Timeline.Header.BlockingWarning" = "You can’t view this user's profile @@ -129,14 +129,14 @@ until they unblock you."; until you unblock them. Your profile looks like this to them."; "Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended."; -"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "تحميل المنشورات المَفقودة"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "تحميل المزيد من المنشورات..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "إظهار المزيد من الردود"; "Common.Controls.Timeline.Timestamp.Now" = "الأن"; -"Scene.AccountList.AddAccount" = "Add Account"; -"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; +"Scene.AccountList.AddAccount" = "إضافة حساب"; +"Scene.AccountList.DismissAccountSwitcher" = "تجاهُل مبدِّل الحساب"; "Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; -"Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment"; +"Scene.Compose.Accessibility.AppendAttachment" = "إضافة مُرفَق"; "Scene.Compose.Accessibility.AppendPoll" = "اضافة استطلاع رأي"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي مخصص للإيموجي"; "Scene.Compose.Accessibility.DisableContentWarning" = "تعطيل تحذير الحتوى"; @@ -151,14 +151,14 @@ uploaded to Mastodon."; "Scene.Compose.Attachment.Video" = "فيديو"; "Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; "Scene.Compose.ComposeAction" = "انشر"; -"Scene.Compose.ContentInputPlaceholder" = "ما الذي يجول ببالك"; +"Scene.Compose.ContentInputPlaceholder" = "أخبِرنا بِما يَجُولُ فِي ذِهنَك"; "Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; -"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Add Attachment - %@"; -"Scene.Compose.Keyboard.DiscardPost" = "Discard Post"; -"Scene.Compose.Keyboard.PublishPost" = "Publish Post"; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "إضافة مُرفَق - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "تجاهُل المنشور"; +"Scene.Compose.Keyboard.PublishPost" = "نَشر المَنشُور"; "Scene.Compose.Keyboard.SelectVisibilityEntry" = "اختر مدى الظهور - %@"; -"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning"; -"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "تبديل تحذير المُحتوى"; +"Scene.Compose.Keyboard.TogglePoll" = "تبديل الاستطلاع"; "Scene.Compose.MediaSelection.Browse" = "تصفح"; "Scene.Compose.MediaSelection.Camera" = "التقط صورة"; "Scene.Compose.MediaSelection.PhotoLibrary" = "مكتبة الصور"; @@ -180,23 +180,23 @@ uploaded to Mastodon."; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "لم أستلم أبدًا بريدا إلكترونيا"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "افتح تطبيق البريد الإلكتروني"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; -"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email"; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "إعادة إرسال البريد الإلكتروني"; "Scene.ConfirmEmail.DontReceiveEmail.Title" = "تحقق من بريدك الإلكتروني"; "Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t."; "Scene.ConfirmEmail.OpenEmailApp.Mail" = "البريد"; -"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client"; -"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox."; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "فتح عميل البريد الإلكتروني"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "تحقَّق من بريدك الوارِد."; "Scene.ConfirmEmail.Subtitle" = "لقد أرسلنا للتو رسالة بريد إلكتروني إلى %@، اضغط على الرابط لتأكيد حسابك."; "Scene.ConfirmEmail.Title" = "شيء واحد أخير."; "Scene.Favorite.Title" = "مفضلتك"; -"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "إظهار منشورات جديدة"; "Scene.HomeTimeline.NavigationBarState.Offline" = "غير متصل"; "Scene.HomeTimeline.NavigationBarState.Published" = "تم نشره!"; "Scene.HomeTimeline.NavigationBarState.Publishing" = "جارٍ نشر المشاركة…"; "Scene.HomeTimeline.Title" = "الخيط الرئيسي"; "Scene.Notification.Keyobard.ShowEverything" = "إظهار كل شيء"; -"Scene.Notification.Keyobard.ShowMentions" = "Show Mentions"; +"Scene.Notification.Keyobard.ShowMentions" = "إظهار الإشارات"; "Scene.Notification.Title.Everything" = "الكل"; "Scene.Notification.Title.Mentions" = "الإشارات"; "Scene.Notification.UserFavorited Your Post" = "أضاف %@ منشورك إلى مفضلته"; @@ -204,7 +204,7 @@ uploaded to Mastodon."; "Scene.Notification.UserMentionedYou" = "أشار إليك %@"; "Scene.Notification.UserRebloggedYourPost" = "أعاد %@ تدوين مشاركتك"; "Scene.Notification.UserRequestedToFollowYou" = "طلب %@ متابعتك"; -"Scene.Notification.UserYourPollHasEnded" = "%@ Your poll has ended"; +"Scene.Notification.UserYourPollHasEnded" = "%@ اِنتهى استطلاعُكَ للرأي"; "Scene.Preview.Keyboard.ClosePreview" = "إغلاق المعاينة"; "Scene.Preview.Keyboard.ShowNext" = "إظهار التالي"; "Scene.Preview.Keyboard.ShowPrevious" = "إظهار السابق"; @@ -213,7 +213,7 @@ uploaded to Mastodon."; "Scene.Profile.Dashboard.Posts" = "منشورات"; "Scene.Profile.Fields.AddRow" = "إضافة صف"; "Scene.Profile.Fields.Placeholder.Content" = "المحتوى"; -"Scene.Profile.Fields.Placeholder.Label" = "Label"; +"Scene.Profile.Fields.Placeholder.Label" = "التسمية"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm to unblock %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "إلغاء حظر الحساب"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@"; @@ -263,7 +263,7 @@ uploaded to Mastodon."; "Scene.Search.Recommend.Accounts.Title" = "حسابات قد تعجبك"; "Scene.Search.Recommend.ButtonText" = "طالع الكل"; "Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention"; -"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ أشخاص يتحدَّثوا"; "Scene.Search.Recommend.HashTag.Title" = "ذات شعبية على ماستدون"; "Scene.Search.SearchBar.Cancel" = "إلغاء"; "Scene.Search.SearchBar.Placeholder" = "البحث عن وسوم أو مستخدمين·ات"; @@ -275,7 +275,7 @@ uploaded to Mastodon."; "Scene.Search.Searching.Segment.People" = "الأشخاص"; "Scene.Search.Searching.Segment.Posts" = "المنشورات"; "Scene.Search.Title" = "بحث"; -"Scene.ServerPicker.Button.Category.Academia" = "academia"; +"Scene.ServerPicker.Button.Category.Academia" = "أكاديمي"; "Scene.ServerPicker.Button.Category.Activism" = "للنشطاء"; "Scene.ServerPicker.Button.Category.All" = "الكل"; "Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "الفئة: الكل"; @@ -285,7 +285,7 @@ uploaded to Mastodon."; "Scene.ServerPicker.Button.Category.Games" = "ألعاب"; "Scene.ServerPicker.Button.Category.General" = "عام"; "Scene.ServerPicker.Button.Category.Journalism" = "صحافة"; -"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Lgbt" = "مجتمع الشواذ"; "Scene.ServerPicker.Button.Category.Music" = "موسيقى"; "Scene.ServerPicker.Button.Category.Regional" = "اقليمي"; "Scene.ServerPicker.Button.Category.Tech" = "تكنولوجيا"; @@ -298,8 +298,8 @@ uploaded to Mastodon."; "Scene.ServerPicker.Label.Category" = "الفئة"; "Scene.ServerPicker.Label.Language" = "اللغة"; "Scene.ServerPicker.Label.Users" = "مستخدمون·ات"; -"Scene.ServerPicker.Title" = "Pick a server, -any server."; +"Scene.ServerPicker.Title" = "اِختر خادِم، +أي خادِم."; "Scene.ServerRules.Button.Confirm" = "انا أوافق"; "Scene.ServerRules.PrivacyPolicy" = "سياسة الخصوصية"; "Scene.ServerRules.Prompt" = "إن اخترت المواصلة، فإنك تخضع لشروط الخدمة وسياسة الخصوصية لـ %@."; @@ -309,38 +309,38 @@ any server."; "Scene.Settings.Footer.MastodonDescription" = "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على غيت هب %@ (%@)"; "Scene.Settings.Keyboard.CloseSettingsWindow" = "إغلاق نافذة الإعدادات"; "Scene.Settings.Section.Appearance.Automatic" = "تلقائي"; -"Scene.Settings.Section.Appearance.Dark" = "Always Dark"; -"Scene.Settings.Section.Appearance.Light" = "Always Light"; +"Scene.Settings.Section.Appearance.Dark" = "مظلمٌ دائِمًا"; +"Scene.Settings.Section.Appearance.Light" = "مضيءٌ دائمًا"; "Scene.Settings.Section.Appearance.Title" = "المظهر"; "Scene.Settings.Section.BoringZone.AccountSettings" = "إعدادات الحساب"; "Scene.Settings.Section.BoringZone.Privacy" = "سياسة الخصوصية"; "Scene.Settings.Section.BoringZone.Terms" = "شروط الخدمة"; "Scene.Settings.Section.BoringZone.Title" = "المنطقة المملة"; -"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post"; -"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post"; +"Scene.Settings.Section.Notifications.Boosts" = "إعادة تدوين منشوراتي"; +"Scene.Settings.Section.Notifications.Favorites" = "الإعجاب بِمنشوراتي"; "Scene.Settings.Section.Notifications.Follows" = "يتابعني"; -"Scene.Settings.Section.Notifications.Mentions" = "Mentions me"; +"Scene.Settings.Section.Notifications.Mentions" = "الإشارة لي"; "Scene.Settings.Section.Notifications.Title" = "الإشعارات"; -"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone"; -"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "أي شخص"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "أي شخص أُتابِعُه"; "Scene.Settings.Section.Notifications.Trigger.Follower" = "مشترِك"; -"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one"; -"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when"; -"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars"; -"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "لا أحد"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "إشعاري عِندَ"; +"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "تعطيل الصور الرمزية المتحرِّكة"; +"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "تعطيل الرموز التعبيرية المتحرِّكَة"; "Scene.Settings.Section.Preference.Title" = "التفضيلات"; -"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode"; -"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links"; +"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "النمط الأسود الداكِن الحقيقي"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "اِستخدام المتصفح الافتراضي لفتح الروابط"; "Scene.Settings.Section.SpicyZone.Clear" = "مسح ذاكرة التخزين المؤقت للوسائط"; "Scene.Settings.Section.SpicyZone.Signout" = "تسجيل الخروج"; "Scene.Settings.Section.SpicyZone.Title" = "المنطقة الحارة"; "Scene.Settings.Title" = "الإعدادات"; "Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed."; "Scene.SuggestionAccount.Title" = "ابحث عن أشخاص لمتابعتهم"; -"Scene.Thread.BackTitle" = "Post"; -"Scene.Thread.Title" = "Post from %@"; +"Scene.Thread.BackTitle" = "منشور"; +"Scene.Thread.Title" = "مَنشور مِن %@"; "Scene.Welcome.Slogan" = "Social networking back in your hands."; -"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; +"Scene.Wizard.AccessibilityHint" = "انقر نقرًا مزدوجًا لتجاهل النافذة المنبثقة"; "Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.NewInMastodon" = "جديد في ماستودون"; \ No newline at end of file diff --git a/Mastodon/Resources/ar.lproj/Localizable.stringsdict b/Mastodon/Resources/ar.lproj/Localizable.stringsdict index e6b0d5f95..e3dee0d80 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/ar.lproj/Localizable.stringsdict @@ -15,21 +15,21 @@ zero %ld unread notification one - 1 unread notification + إشعار واحِد غير مقروء two - %ld unread notification + إشعاران غير مقروءان few %ld unread notification many - %ld unread notification + %ld إشعارًا غيرَ مقروء other - %ld unread notification + %ld إشعار غير مقروء a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ + تمَّ تجاوز حدّ الإدخال %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -37,23 +37,23 @@ NSStringFormatValueTypeKey ld zero - %ld characters + لا حرف one - 1 character + حرفٌ واحِد two - %ld characters + حرفان اثنان few - %ld characters + %ld حُرُوف many - %ld characters + %ld حرفًا other - %ld characters + %ld حَرف a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - Input limit remains %#@character_count@ + يتبقَّى على حدّ الإدخال %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -61,17 +61,17 @@ NSStringFormatValueTypeKey ld zero - %ld characters + لا حرف one - 1 character + حرفٌ واحِد two - %ld characters + حرفان اثنان few - %ld characters + %ld حُرُوف many - %ld characters + %ld حرفًا other - %ld characters + %ld حَرف plural.count.metric_formatted.post @@ -85,17 +85,17 @@ NSStringFormatValueTypeKey ld zero - posts + لا منشور one - post + منشور two - posts + منشوران few - posts + منشورات many - posts + منشورًا other - posts + منشور plural.count.post @@ -109,17 +109,17 @@ NSStringFormatValueTypeKey ld zero - %ld posts + لا منشور one - 1 post + منشورٌ واحِد two - %ld posts + منشورانِ اثنان few - %ld posts + %ld منشورات many - %ld posts + %ld منشورًا other - %ld posts + %ld منشور plural.count.favorite @@ -133,17 +133,17 @@ NSStringFormatValueTypeKey ld zero - %ld favorites + لا إعجاب one - 1 favorite + إعجابٌ واحِد two - %ld favorites + إعجابانِ اثنان few - %ld favorites + %ld إعجابات many - %ld favorites + %ld إعجابًا other - %ld favorites + %ld إعجاب plural.count.reblog @@ -157,17 +157,17 @@ NSStringFormatValueTypeKey ld zero - %ld reblogs + لا إعاد تدوين one - 1 reblog + إعادةُ تدوينٍ واحِدة two - %ld reblogs + إعادتا تدوين few - %ld reblogs + %ld إعاداتِ تدوين many - %ld reblogs + %ld إعادةٍ للتدوين other - %ld reblogs + %ld إعادة تدوين plural.count.vote @@ -181,17 +181,17 @@ NSStringFormatValueTypeKey ld zero - %ld votes + لا صوت one - 1 vote + صوتٌ واحِد two - %ld votes + صوتانِ اثنان few - %ld votes + %ld أصوات many - %ld votes + %ld صوتًا other - %ld votes + %ld صوت plural.count.voter @@ -205,17 +205,17 @@ NSStringFormatValueTypeKey ld zero - %ld voters + لا مُصوِّتون one - 1 voter + مُصوِّتٌ واحِد two - %ld voters + مُصوِّتانِ اثنان few - %ld voters + %ld مُصوِّتين many - %ld voters + %ld مُصوِّتًا other - %ld voters + %ld مُصوِّت plural.people_talking @@ -229,17 +229,17 @@ NSStringFormatValueTypeKey ld zero - %ld people talking + لا أحَدَ يتحدَّث one - 1 people talking + شخصٌ واحدٌ يتحدَّث two - %ld people talking + شخصانِ اثنان يتحدَّثا few - %ld people talking + %ld أشخاصٍ يتحدَّثون many - %ld people talking + %ld شخصًا يتحدَّثون other - %ld people talking + %ld شخصٍ يتحدَّثون plural.count.following @@ -253,17 +253,17 @@ NSStringFormatValueTypeKey ld zero - %ld following + لا مُتابَع one - 1 following + مُتابَعٌ واحد two - %ld following + مُتابَعانِ few - %ld following + %ld مُتابَعين many - %ld following + %ld مُتابَعًا other - %ld following + %ld مُتابَع plural.count.follower @@ -279,15 +279,15 @@ zero %ld followers one - 1 follower + مُتابِعٌ واحد two - %ld followers + مُتابِعانِ اثنان few - %ld followers + %ld مُتابِعين many - %ld followers + %ld مُتابِعًا other - %ld followers + %ld مُتابِع date.year.left @@ -301,17 +301,17 @@ NSStringFormatValueTypeKey ld zero - %ld years left + تتبقى لَحظة one - 1 year left + تتبقى سنة two - %ld years left + تتبقى سنتين few - %ld years left + تتبقى %ld سنوات many - %ld years left + تتبقى %ld سنةً other - %ld years left + تتبقى %ld سنة date.month.left @@ -325,17 +325,17 @@ NSStringFormatValueTypeKey ld zero - %ld months left + تتبقى لَحظة one - 1 months left + يتبقى شهر two - %ld months left + يتبقى شهرين few - %ld months left + يتبقى %ld أشهر many - %ld months left + يتبقى %ld شهرًا other - %ld months left + يتبقى %ld شهر date.day.left @@ -349,17 +349,17 @@ NSStringFormatValueTypeKey ld zero - %ld days left + تتبقى لحظة one - 1 day left + يتبقى يوم two - %ld days left + يتبقى يومين few - %ld days left + يتبقى %ld أيام many - %ld days left + يتبقى %ld يومًا other - %ld days left + يتبقى %ld يوم date.hour.left @@ -373,17 +373,17 @@ NSStringFormatValueTypeKey ld zero - %ld hours left + تتبقى لَحظة one - 1 hour left + تتبقى ساعة two - %ld hours left + تتبقى ساعتين few - %ld hours left + تتبقى %ld ساعات many - %ld hours left + تتبقى %ld ساعةً other - %ld hours left + تتبقى %ld ساعة date.minute.left @@ -397,17 +397,17 @@ NSStringFormatValueTypeKey ld zero - %ld minutes left + تتبقى لَحظة one - 1 minute left + تتبقى دقيقة two - %ld minutes left + تتبقى دقيقتين few - %ld minutes left + تتبقى %ld دقائق many - %ld minutes left + تتبقى %ld دقيقةً other - %ld minutes left + تتبقى %ld دقيقة date.second.left @@ -421,17 +421,17 @@ NSStringFormatValueTypeKey ld zero - %ld seconds left + تتبقى لَحظة one - 1 second left + تتبقى ثانية two - %ld seconds left + تتبقى ثانيتين few - %ld seconds left + تتبقى %ld ثوان many - %ld seconds left + تتبقى %ld ثانيةً other - %ld seconds left + تتبقى %ld ثانية date.year.ago.abbr @@ -445,17 +445,17 @@ NSStringFormatValueTypeKey ld zero - %ldy ago + مُنذُ لَحظة one - 1y ago + مُنذُ سنة two - %ldy ago + مُنذُ سنتين few - %ldy ago + مُنذُ %ld سنين many - %ldy ago + مُنذُ %ld سنةً other - %ldy ago + مُنذُ %ld سنة date.month.ago.abbr @@ -469,17 +469,17 @@ NSStringFormatValueTypeKey ld zero - %ldM ago + مُنذُ لَحظة one - 1M ago + مُنذُ شهر two - %ldM ago + مُنذُ شهرين few - %ldM ago + مُنذُ %ld أشهُر many - %ldM ago + مُنذُ %ld شهرًا other - %ldM ago + مُنذُ %ld شهر date.day.ago.abbr @@ -493,17 +493,17 @@ NSStringFormatValueTypeKey ld zero - %ldd ago + مُنذُ لَحظة one - 1d ago + مُنذُ يوم two - %ldd ago + مُنذُ يومين few - %ldd ago + مُنذُ %ld أيام many - %ldd ago + مُنذُ %ld يومًا other - %ldd ago + مُنذُ %ld يوم date.hour.ago.abbr @@ -517,17 +517,17 @@ NSStringFormatValueTypeKey ld zero - %ldh ago + مُنذُ لَحظة one - 1h ago + مُنذُ ساعة two - %ldh ago + مُنذُ ساعتين few - %ldh ago + مُنذُ %ld ساعات many - %ldh ago + مُنذُ %ld ساعةًَ other - %ldh ago + مُنذُ %ld ساعة date.minute.ago.abbr @@ -541,17 +541,17 @@ NSStringFormatValueTypeKey ld zero - %ldm ago + مُنذُ لَحظة one - 1m ago + مُنذُ دقيقة two - %ldm ago + مُنذُ دقيقتان few - %ldm ago + مُنذُ %ld دقائق many - %ldm ago + مُنذُ %ld دقيقةً other - %ldm ago + مُنذُ %ld دقيقة date.second.ago.abbr @@ -565,17 +565,17 @@ NSStringFormatValueTypeKey ld zero - %lds ago + مُنذُ لَحظة one - 1s ago + مُنذُ ثانية two - %lds ago + مُنذُ ثانيتين few - %lds ago + مُنذُ %ld ثوان many - %lds ago + مُنذُ %ld ثانية other - %lds ago + مُنذُ %ld ثانية diff --git a/Mastodon/Resources/ca.lproj/Localizable.stringsdict b/Mastodon/Resources/ca.lproj/Localizable.stringsdict index cc7312938..140185bad 100644 --- a/Mastodon/Resources/ca.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/ca.lproj/Localizable.stringsdict @@ -21,7 +21,7 @@ a11y.plural.count.input_limit_exceeds NSStringLocalizedFormatKey - El límit d’entrada supera a %#@character_count@ + El límit de la entrada supera a %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -37,7 +37,7 @@ a11y.plural.count.input_limit_remains NSStringLocalizedFormatKey - El límit d’entrada continua sent %#@character_count@ + El límit de la entrada continua sent %#@character_count@ character_count NSStringFormatSpecTypeKey @@ -111,7 +111,7 @@ one 1 impuls other - %ld impuls + %ld impulsos plural.count.vote @@ -301,9 +301,9 @@ NSStringFormatValueTypeKey ld one - fa 1a + fa 1 any other - fa %ldy anys + fa %ld anys date.month.ago.abbr @@ -317,9 +317,9 @@ NSStringFormatValueTypeKey ld one - fa 1M + fa 1 mes other - fa %ldM mesos + fa %ld mesos date.day.ago.abbr @@ -333,9 +333,9 @@ NSStringFormatValueTypeKey ld one - fa 1d + fa 1 día other - fa %ldd dies + fa %ld dies date.hour.ago.abbr @@ -351,7 +351,7 @@ one fa 1h other - fa %ldh hores + fa %ld hores date.minute.ago.abbr @@ -365,9 +365,9 @@ NSStringFormatValueTypeKey ld one - fa 1m + fa 1 minut other - fa %ldm minuts + fa %ld minuts date.second.ago.abbr @@ -381,9 +381,9 @@ NSStringFormatValueTypeKey ld one - fa 1s + fa 1 segon other - fa %lds seg + fa %ld segons diff --git a/Mastodon/Resources/de.lproj/Localizable.strings b/Mastodon/Resources/de.lproj/Localizable.strings index cc92b8e77..2780723ed 100644 --- a/Mastodon/Resources/de.lproj/Localizable.strings +++ b/Mastodon/Resources/de.lproj/Localizable.strings @@ -133,9 +133,9 @@ Dein Profil sieht für diesen Benutzer auch so aus."; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Lade fehlende Beiträge..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Weitere Antworten anzeigen"; "Common.Controls.Timeline.Timestamp.Now" = "Gerade"; -"Scene.AccountList.AddAccount" = "Add Account"; +"Scene.AccountList.AddAccount" = "Konto hinzufügen"; "Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; -"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; +"Scene.AccountList.TabBarHint" = "Aktuell ausgewähltes Profil: %@. Doppeltippen dann gedrückt halten, um den Kontoschalter anzuzeigen"; "Scene.Compose.Accessibility.AppendAttachment" = "Anhang hinzufügen"; "Scene.Compose.Accessibility.AppendPoll" = "Umfrage hinzufügen"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Benutzerdefinierter Emojiwähler"; @@ -340,6 +340,6 @@ beliebigen Server."; "Scene.Thread.BackTitle" = "Beitrag"; "Scene.Thread.Title" = "Beitrag von %@"; "Scene.Welcome.Slogan" = "Soziale Netzwerke wieder in deinen Händen."; -"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; -"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.AccessibilityHint" = "Doppeltippen, um diesen Assistenten zu schließen"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Wechsel zwischen mehreren Konten durch drücken der Profil-Schaltfläche."; +"Scene.Wizard.NewInMastodon" = "Neu in Mastodon"; \ No newline at end of file diff --git a/Mastodon/Resources/de.lproj/Localizable.stringsdict b/Mastodon/Resources/de.lproj/Localizable.stringsdict index c868bdc0f..66b7f2a2d 100644 --- a/Mastodon/Resources/de.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/de.lproj/Localizable.stringsdict @@ -13,9 +13,9 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + 1 ungelesene Benachrichtigung other - %ld unread notification + %ld ungelesene Benachrichtigungen a11y.plural.count.input_limit_exceeds diff --git a/Mastodon/Resources/gd-GB.lproj/Localizable.strings b/Mastodon/Resources/gd-GB.lproj/Localizable.strings index f24bd24e9..6c01adb0a 100644 --- a/Mastodon/Resources/gd-GB.lproj/Localizable.strings +++ b/Mastodon/Resources/gd-GB.lproj/Localizable.strings @@ -133,9 +133,9 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san."; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "A’ luchdadh nam post a tha a dhìth…"; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Seall barrachd freagairtean"; "Common.Controls.Timeline.Timestamp.Now" = "An-dràsta"; -"Scene.AccountList.AddAccount" = "Add Account"; -"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; -"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; +"Scene.AccountList.AddAccount" = "Cuir cunntas ris"; +"Scene.AccountList.DismissAccountSwitcher" = "Leig seachad taghadh a’ chunntais"; +"Scene.AccountList.TabBarHint" = "A’ phròifil air a taghadh: %@. Thoir gnogag dhùbailte is cùm sìos a ghearradh leum gu cunntas eile"; "Scene.Compose.Accessibility.AppendAttachment" = "Cuir ceanglachan ris"; "Scene.Compose.Accessibility.AppendPoll" = "Cuir cunntas-bheachd ris"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "Roghnaichear nan Emoji gnàthaichte"; @@ -340,6 +340,6 @@ thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad."; "Scene.Thread.Title" = "Post le %@"; "Scene.Welcome.Slogan" = "A’ cur nan lìonraidhean sòisealta ’nad làmhan fhèin."; -"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; -"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.AccessibilityHint" = "Thoir gnogag dhùbailte a’ leigeil seachad an draoidh seo"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Geàrr leum eadar iomadh cunntas le cumail sìos putan na pròifil."; +"Scene.Wizard.NewInMastodon" = "Na tha ùr ann am Mastodon"; \ No newline at end of file diff --git a/Mastodon/Resources/gd-GB.lproj/Localizable.stringsdict b/Mastodon/Resources/gd-GB.lproj/Localizable.stringsdict index 41e592a5e..7a54f553e 100644 --- a/Mastodon/Resources/gd-GB.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/gd-GB.lproj/Localizable.stringsdict @@ -13,13 +13,13 @@ NSStringFormatValueTypeKey ld one - 1 unread notification + %ld bhrath nach deach a leughadh two - %ld unread notification + %ld bhrath nach deach a leughadh few - %ld unread notification + %ld brathan nach deach a leughadh other - %ld unread notification + %ld brath nach deach a leughadh a11y.plural.count.input_limit_exceeds diff --git a/Mastodon/Resources/ja.lproj/Localizable.strings b/Mastodon/Resources/ja.lproj/Localizable.strings index e83278e34..beadccf22 100644 --- a/Mastodon/Resources/ja.lproj/Localizable.strings +++ b/Mastodon/Resources/ja.lproj/Localizable.strings @@ -129,7 +129,7 @@ "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "読込中..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "リプライをもっとみる"; "Common.Controls.Timeline.Timestamp.Now" = "今"; -"Scene.AccountList.AddAccount" = "Add Account"; +"Scene.AccountList.AddAccount" = "アカウントを追加"; "Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; "Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; "Scene.Compose.Accessibility.AppendAttachment" = "アタッチメントの追加"; @@ -332,8 +332,7 @@ "Scene.SuggestionAccount.Title" = "フォローする人を探す"; "Scene.Thread.BackTitle" = "投稿"; "Scene.Thread.Title" = "%@の投稿"; -"Scene.Welcome.Slogan" = "Social networking -back in your hands."; +"Scene.Welcome.Slogan" = "ソーシャルネットワーキングを、あなたの手の中に."; "Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; -"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "プロフィールボタンを押して複数のアカウントを切り替えます。"; +"Scene.Wizard.NewInMastodon" = "Mastodon の新機能"; \ No newline at end of file diff --git a/Mastodon/Resources/ja.lproj/Localizable.stringsdict b/Mastodon/Resources/ja.lproj/Localizable.stringsdict index 0300d9dc3..c51a9a29d 100644 --- a/Mastodon/Resources/ja.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/ja.lproj/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld unread notification + %ld 件の未読通知 a11y.plural.count.input_limit_exceeds @@ -27,7 +27,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 文字 a11y.plural.count.input_limit_remains @@ -41,7 +41,7 @@ NSStringFormatValueTypeKey ld other - %ld characters + %ld 文字 plural.count.metric_formatted.post @@ -111,7 +111,7 @@ NSStringFormatValueTypeKey ld other - %ld votes + %ld票 plural.count.voter @@ -195,7 +195,7 @@ NSStringFormatValueTypeKey ld other - %ld months left + %ldか月前 date.day.left @@ -279,7 +279,7 @@ NSStringFormatValueTypeKey ld other - %ldM ago + %ld分前 date.day.ago.abbr diff --git a/Mastodon/Resources/ku-TR.lproj/InfoPlist.strings b/Mastodon/Resources/ku-TR.lproj/InfoPlist.strings new file mode 100644 index 000000000..669ecfacf --- /dev/null +++ b/Mastodon/Resources/ku-TR.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +"NSCameraUsageDescription" = "Bo kişandina wêneyê ji bo rewşa şandiyan tê bikaranîn"; +"NSPhotoLibraryAddUsageDescription" = "Ji bo tomarkirina wêneyê di pirtûkxaneya wêneyan de tê bikaranîn"; +"NewPostShortcutItemTitle" = "Şandiya nû"; +"SearchShortcutItemTitle" = "Bigere"; \ No newline at end of file diff --git a/Mastodon/Resources/ku-TR.lproj/Localizable.strings b/Mastodon/Resources/ku-TR.lproj/Localizable.strings new file mode 100644 index 000000000..345f10cf9 --- /dev/null +++ b/Mastodon/Resources/ku-TR.lproj/Localizable.strings @@ -0,0 +1,346 @@ +"Common.Alerts.BlockDomain.BlockEntireDomain" = "Navperê asteng bike"; +"Common.Alerts.BlockDomain.Title" = "Tu ji xwe bawerî, bi rastî tu dixwazî hemû %@ asteng bikî? Di gelek rewşan de asteng kirin an jî bêdeng kirin têrê dike û tê tercîh kirin. Tu nikarî naveroka vê navperê di demnameyê an jî agahdariyên xwe de bibînî. Şopînerên te yê di vê navperê were jêbirin."; +"Common.Alerts.CleanCache.Message" = "Pêşbîra %@ biserketî hate paqijkirin."; +"Common.Alerts.CleanCache.Title" = "Pêşbîrê paqij bike"; +"Common.Alerts.Common.PleaseTryAgain" = "Ji kerema xwe dîsa biceribîne."; +"Common.Alerts.Common.PleaseTryAgainLater" = "Ji kerema xwe paşê dîsa biceribîne."; +"Common.Alerts.DeletePost.Delete" = "Jê bibe"; +"Common.Alerts.DeletePost.Title" = "Ma tu dixwazî vê şandiyê jê bibî?"; +"Common.Alerts.DiscardPostContent.Message" = "Piştrast bikin ku naveroka posteyê ya hatîye nivîsandin jê bibin."; +"Common.Alerts.DiscardPostContent.Title" = "Reşnivîs jêbibe"; +"Common.Alerts.EditProfileFailure.Message" = "Nikare profîlê serrast bike. Jkx dîsa biceribîne."; +"Common.Alerts.EditProfileFailure.Title" = "Çewtiya profîlê biguherîne"; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "Nikare ji bêtirî yek vîdyoyekê tevlî şandiyê bike."; +"Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "Nikare vîdyoyekê tevlî şandiyê ku berê wêne tê de heye bike."; +"Common.Alerts.PublishPostFailure.Message" = "Weşandina şandiyê têkçû. +Jkx girêdana înternetê xwe kontrol bike."; +"Common.Alerts.PublishPostFailure.Title" = "Weşandin têkçû"; +"Common.Alerts.SavePhotoFailure.Message" = "Ji kerema xwe destûra gihîştina pirtûkxaneya wêneyê çalak bikin da ku wêneyê hilînin."; +"Common.Alerts.SavePhotoFailure.Title" = "Tomarkirina wêneyê têkçû"; +"Common.Alerts.ServerError.Title" = "Çewtiya rajekar"; +"Common.Alerts.SignOut.Confirm" = "Derkeve"; +"Common.Alerts.SignOut.Message" = "Ma tu dixwazî ku derkevî?"; +"Common.Alerts.SignOut.Title" = "Derkeve"; +"Common.Alerts.SignUpFailure.Title" = "Tomarkirin têkçû"; +"Common.Alerts.VoteFailure.PollEnded" = "Rapirsîya qediya"; +"Common.Alerts.VoteFailure.Title" = "Dengdayîn têkçû"; +"Common.Controls.Actions.Add" = "Tevlî bike"; +"Common.Controls.Actions.Back" = "Vegere"; +"Common.Controls.Actions.BlockDomain" = "%@ asteng bike"; +"Common.Controls.Actions.Cancel" = "Dev jê berde"; +"Common.Controls.Actions.Confirm" = "Bipejirîne"; +"Common.Controls.Actions.Continue" = "Bidomîne"; +"Common.Controls.Actions.CopyPhoto" = "Wêne kopî bikin"; +"Common.Controls.Actions.Delete" = "Jê bibe"; +"Common.Controls.Actions.Discard" = "Biavêje"; +"Common.Controls.Actions.Done" = "Qediya"; +"Common.Controls.Actions.Edit" = "Serrast bike"; +"Common.Controls.Actions.FindPeople" = "Kesên ku bişopînin bibînin"; +"Common.Controls.Actions.ManuallySearch" = "Ji devlê i destan lêgerînê bike"; +"Common.Controls.Actions.Next" = "Pêş"; +"Common.Controls.Actions.Ok" = "BAŞ E"; +"Common.Controls.Actions.Open" = "Veke"; +"Common.Controls.Actions.OpenInSafari" = "Di Safariyê de veke"; +"Common.Controls.Actions.Preview" = "Pêşdîtin"; +"Common.Controls.Actions.Previous" = "Paş"; +"Common.Controls.Actions.Remove" = "Rake"; +"Common.Controls.Actions.Reply" = "Bersivê bide"; +"Common.Controls.Actions.ReportUser" = "%@ ragihîne"; +"Common.Controls.Actions.Save" = "Tomar bike"; +"Common.Controls.Actions.SavePhoto" = "Wêneyê hilîne"; +"Common.Controls.Actions.SeeMore" = "Bêtir bibîne"; +"Common.Controls.Actions.Settings" = "Sazkarî"; +"Common.Controls.Actions.Share" = "Parve bike"; +"Common.Controls.Actions.SharePost" = "Şandiyê parve bike"; +"Common.Controls.Actions.ShareUser" = "%@ parve bike"; +"Common.Controls.Actions.SignIn" = "Têkeve"; +"Common.Controls.Actions.SignUp" = "Tomar bibe"; +"Common.Controls.Actions.Skip" = "Derbas bike"; +"Common.Controls.Actions.TakePhoto" = "Wêne bikişîne"; +"Common.Controls.Actions.TryAgain" = "Dîsa biceribîne"; +"Common.Controls.Actions.UnblockDomain" = "%@ asteng neke"; +"Common.Controls.Friendship.Block" = "Asteng bike"; +"Common.Controls.Friendship.BlockDomain" = "%@ asteng bike"; +"Common.Controls.Friendship.BlockUser" = "%@ asteng bike"; +"Common.Controls.Friendship.Blocked" = "Astengkirî"; +"Common.Controls.Friendship.EditInfo" = "Zanyariyan serrast bike"; +"Common.Controls.Friendship.Follow" = "Bişopîne"; +"Common.Controls.Friendship.Following" = "Dişopîne"; +"Common.Controls.Friendship.Mute" = "Bêdeng bike"; +"Common.Controls.Friendship.MuteUser" = "%@ bêdeng bike"; +"Common.Controls.Friendship.Muted" = "Bêdengkirî"; +"Common.Controls.Friendship.Pending" = "Tê nirxandin"; +"Common.Controls.Friendship.Request" = "Daxwazên şopandinê"; +"Common.Controls.Friendship.Unblock" = "Astengiyê rake"; +"Common.Controls.Friendship.UnblockUser" = "%@ asteng neke"; +"Common.Controls.Friendship.Unmute" = "Bêdeng neke"; +"Common.Controls.Friendship.UnmuteUser" = "%@ bêdeng neke"; +"Common.Controls.Keyboard.Common.ComposeNewPost" = "Şandiyeke nû binivsîne"; +"Common.Controls.Keyboard.Common.OpenSettings" = "Sazkariyan Veke"; +"Common.Controls.Keyboard.Common.ShowFavorites" = "Bijarteyan nîşan bide"; +"Common.Controls.Keyboard.Common.SwitchToTab" = "Biguherîne bo %@"; +"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Beşa paşê"; +"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Beşa berê"; +"Common.Controls.Keyboard.Timeline.NextStatus" = "Şandiya pêş"; +"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Profîla nivîskaran veke"; +"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Profîla nivîskaran veke"; +"Common.Controls.Keyboard.Timeline.OpenStatus" = "Şandiyê veke"; +"Common.Controls.Keyboard.Timeline.PreviewImage" = "Wêneya pêşdîtinê"; +"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Şandeya paş"; +"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Bersivê bide şandiyê"; +"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Hişyariya naverokê veke/bigire"; +"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Di postê da Bijartin veke/bigire"; +"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Reblog on Post"; +"Common.Controls.Status.Actions.Favorite" = "Bijartî"; +"Common.Controls.Status.Actions.Menu" = "Menû"; +"Common.Controls.Status.Actions.Reblog" = "Ji nû ve blog"; +"Common.Controls.Status.Actions.Reply" = "Bersivê bide"; +"Common.Controls.Status.Actions.Unfavorite" = "Nebijare"; +"Common.Controls.Status.Actions.Unreblog" = "Ji nû ve blogkirin betal bikin"; +"Common.Controls.Status.ContentWarning" = "Hişyariya naverokê"; +"Common.Controls.Status.MediaContentWarning" = "Ji bo aşkerakirinê derekî bitikîne"; +"Common.Controls.Status.Poll.Closed" = "Girtî"; +"Common.Controls.Status.Poll.Vote" = "Deng"; +"Common.Controls.Status.ShowPost" = "Şandiyê nîşan bide"; +"Common.Controls.Status.ShowUserProfile" = "Profîla bikarhêner nîşan bide"; +"Common.Controls.Status.Tag.Email" = "E-name"; +"Common.Controls.Status.Tag.Emoji" = "E-name"; +"Common.Controls.Status.Tag.Hashtag" = "Etîket"; +"Common.Controls.Status.Tag.Link" = "Girêdan"; +"Common.Controls.Status.Tag.Mention" = "Behs"; +"Common.Controls.Status.Tag.Url" = "URL"; +"Common.Controls.Status.UserReblogged" = "%@ ji nû ve hat blogkirin"; +"Common.Controls.Status.UserRepliedTo" = "Bersiv da %@"; +"Common.Controls.Tabs.Home" = "Serrûpel"; +"Common.Controls.Tabs.Notification" = "Agahdarî"; +"Common.Controls.Tabs.Profile" = "Profîl"; +"Common.Controls.Tabs.Search" = "Bigere"; +"Common.Controls.Timeline.Filtered" = "Parzûnkirî"; +"Common.Controls.Timeline.Header.BlockedWarning" = "Tu nikarî profîla vî bikarhênerî bibînî +heta ku astengîya te rakin."; +"Common.Controls.Timeline.Header.BlockingWarning" = "Tu nikarî profîla vî bikarhênerî bibînî +Heta ku tu wan asteng bikî. +Profîla te ji wan ra wiha xuya dike."; +"Common.Controls.Timeline.Header.NoStatusFound" = "Şandî nehate dîtin"; +"Common.Controls.Timeline.Header.SuspendedWarning" = "Ev bikarhêner hat sekinandin."; +"Common.Controls.Timeline.Header.UserBlockedWarning" = "Tu nikarî profîla %@ bibînî +Heta ku astengîya te rakin."; +"Common.Controls.Timeline.Header.UserBlockingWarning" = "Tu nikarî profîla %@ bibînî +Heta ku tu wan asteng bikî. +Profîla te ji wan ra wiha xuya dike."; +"Common.Controls.Timeline.Header.UserSuspendedWarning" = "Hesaba %@ hat sekinandin."; +"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Barkirina posteyên kêm"; +"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Barkirina posteyên kêm..."; +"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Bêtir bersivan nîşan bide"; +"Common.Controls.Timeline.Timestamp.Now" = "Niha"; +"Scene.AccountList.AddAccount" = "Ajimêr tevlî bike"; +"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; +"Scene.AccountList.TabBarHint" = "Profîla hilbijartî ya niha: %@. Du caran bitikîne û paşê dest bide ser da ku guhêrbara ajimêr were nîşandan"; +"Scene.Compose.Accessibility.AppendAttachment" = "Pêvek tevlî bike"; +"Scene.Compose.Accessibility.AppendPoll" = "Rapirsî tevlî bike"; +"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker"; +"Scene.Compose.Accessibility.DisableContentWarning" = "Hişyariya naverokê neçalak bike"; +"Scene.Compose.Accessibility.EnableContentWarning" = "Enable Content Warning"; +"Scene.Compose.Accessibility.PostVisibilityMenu" = "Menuya Xuyabûna Şandiyê"; +"Scene.Compose.Accessibility.RemovePoll" = "Rapirsî rake"; +"Scene.Compose.Attachment.AttachmentBroken" = "Ev %@ naxebite û nayê barkirin + li ser Mastodon."; +"Scene.Compose.Attachment.DescriptionPhoto" = "Describe the photo for the visually-impaired..."; +"Scene.Compose.Attachment.DescriptionVideo" = "Describe the video for the visually-impaired..."; +"Scene.Compose.Attachment.Photo" = "wêne"; +"Scene.Compose.Attachment.Video" = "vîdyo"; +"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add"; +"Scene.Compose.ComposeAction" = "Biweşîne"; +"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind"; +"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here..."; +"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Pêvek lê zêde bike - %@"; +"Scene.Compose.Keyboard.DiscardPost" = "Şandî bihelîne"; +"Scene.Compose.Keyboard.PublishPost" = "Şandiye bide weşan"; +"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Xuyanîbûn hilbijêre - %@"; +"Scene.Compose.Keyboard.ToggleContentWarning" = "Hişyariya naverokê veke/bigire"; +"Scene.Compose.Keyboard.TogglePoll" = "Anketê veke/bigire"; +"Scene.Compose.MediaSelection.Browse" = "Bigere"; +"Scene.Compose.MediaSelection.Camera" = "Wêne bikişîne"; +"Scene.Compose.MediaSelection.PhotoLibrary" = "Wênegeh"; +"Scene.Compose.Poll.DurationTime" = "Dirêjî: %@"; +"Scene.Compose.Poll.OneDay" = "1 Roj"; +"Scene.Compose.Poll.OneHour" = "1 Demjimêr"; +"Scene.Compose.Poll.OptionNumber" = "Vebijêrk %ld"; +"Scene.Compose.Poll.SevenDays" = "7 Roj"; +"Scene.Compose.Poll.SixHours" = "6 Demjimêr"; +"Scene.Compose.Poll.ThirtyMinutes" = "30 xulek"; +"Scene.Compose.Poll.ThreeDays" = "3 Roj"; +"Scene.Compose.ReplyingToUser" = "bersiv bide %@"; +"Scene.Compose.Title.NewPost" = "Şandiya nû"; +"Scene.Compose.Title.NewReply" = "Bersiva nû"; +"Scene.Compose.Visibility.Direct" = "Tenê mirovên ku min qalkirî"; +"Scene.Compose.Visibility.Private" = "Tenê şopîneran"; +"Scene.Compose.Visibility.Public" = "Gelemperî"; +"Scene.Compose.Visibility.Unlisted" = "Nerêzokkirî"; +"Scene.ConfirmEmail.Button.DontReceiveEmail" = "Min hîç e-nameyeke nesitand"; +"Scene.ConfirmEmail.Button.OpenEmailApp" = "Sepana e-nameyê veke"; +"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Kontrol bike ka navnîşana e-nameya te rast e û her wiha peldanka xwe ya spam."; +"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "E-namyê yê dîsa bişîne"; +"Scene.ConfirmEmail.DontReceiveEmail.Title" = "E-nameyê xwe kontrol bike"; +"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t."; +"Scene.ConfirmEmail.OpenEmailApp.Mail" = "E-name"; +"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Rajegirê e-nameyê veke"; +"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox."; +"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@, +tap the link to confirm your account."; +"Scene.ConfirmEmail.Title" = "Tiştekî dawî."; +"Scene.Favorite.Title" = "Bijareyên te"; +"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Şandiyên nû bibîne"; +"Scene.HomeTimeline.NavigationBarState.Offline" = "Derhêl"; +"Scene.HomeTimeline.NavigationBarState.Published" = "Hate weşandin!"; +"Scene.HomeTimeline.NavigationBarState.Publishing" = "Şandî tê weşandin..."; +"Scene.HomeTimeline.Title" = "Serrûpel"; +"Scene.Notification.Keyobard.ShowEverything" = "Her tiştî nîşan bide"; +"Scene.Notification.Keyobard.ShowMentions" = "Behskirîya nîşan bike"; +"Scene.Notification.Title.Everything" = "Her tişt"; +"Scene.Notification.Title.Mentions" = "Behs"; +"Scene.Notification.UserFavorited Your Post" = "%@ posta we bijarte"; +"Scene.Notification.UserFollowedYou" = "%@ te şopand"; +"Scene.Notification.UserMentionedYou" = "%@ behsa te kir"; +"Scene.Notification.UserRebloggedYourPost" = "%@ posta we ji nû ve tomar kir"; +"Scene.Notification.UserRequestedToFollowYou" = "%@ daxwaza şopandina te kir"; +"Scene.Notification.UserYourPollHasEnded" = "%@ Anketa te qediya"; +"Scene.Preview.Keyboard.ClosePreview" = "Pêşdîtin bigire"; +"Scene.Preview.Keyboard.ShowNext" = "A pêş nîşan bide"; +"Scene.Preview.Keyboard.ShowPrevious" = "A paş nîşan bide"; +"Scene.Profile.Dashboard.Followers" = "şopîneran"; +"Scene.Profile.Dashboard.Following" = "dişopîne"; +"Scene.Profile.Dashboard.Posts" = "şandîyan"; +"Scene.Profile.Fields.AddRow" = "Rêzê lê zêde bike"; +"Scene.Profile.Fields.Placeholder.Content" = "Naverok"; +"Scene.Profile.Fields.Placeholder.Label" = "Nîşan"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Ji bo rakirina blokê bipejirin %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Hesabê ji bloke rake"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Ji bo vekirina bê dengkirinê bipejirin %@"; +"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Hesabê ji bê deng rake"; +"Scene.Profile.SegmentedControl.Media" = "Medya"; +"Scene.Profile.SegmentedControl.Posts" = "Şandîyan"; +"Scene.Profile.SegmentedControl.Replies" = "Bersivan"; +"Scene.Register.Error.Item.Agreement" = "Lihevhatin"; +"Scene.Register.Error.Item.Email" = "E-name"; +"Scene.Register.Error.Item.Locale" = "Herêm"; +"Scene.Register.Error.Item.Password" = "Şîfre"; +"Scene.Register.Error.Item.Reason" = "Sedem"; +"Scene.Register.Error.Item.Username" = "Navê bikarhêner"; +"Scene.Register.Error.Reason.Accepted" = "%@ divê were qebûlkirin"; +"Scene.Register.Error.Reason.Blank" = "%@ pêwist e"; +"Scene.Register.Error.Reason.Blocked" = "%@ peydekerê e-nameya bêdestûr dihewîne"; +"Scene.Register.Error.Reason.Inclusion" = "%@ nirxeke ku tê destekirin nîn e"; +"Scene.Register.Error.Reason.Invalid" = "%@ ne derbasdar e"; +"Scene.Register.Error.Reason.Reserved" = "%@ peyveke mifteya veqetandî ye"; +"Scene.Register.Error.Reason.Taken" = "%@ jixwe tê bikaranîn"; +"Scene.Register.Error.Reason.TooLong" = "%@ gelekî dirêj e"; +"Scene.Register.Error.Reason.TooShort" = "%@ pir kurt e"; +"Scene.Register.Error.Reason.Unreachable" = "%@ xuya nake"; +"Scene.Register.Error.Special.EmailInvalid" = "Ev ne navnîşana e-nameyek derbasdar e"; +"Scene.Register.Error.Special.PasswordTooShort" = "Şîfre pir kurt e (divê herî kêm 8 tîpan be)"; +"Scene.Register.Error.Special.UsernameInvalid" = "Navê bikarhêner divê tenê tîpên alfanumerîk û binxet hebe"; +"Scene.Register.Error.Special.UsernameTooLong" = "Navê bikarhêner pir dirêj e (ji 30 tîpan dirêjtir nabe)"; +"Scene.Register.Input.Avatar.Delete" = "Jê bibe"; +"Scene.Register.Input.DisplayName.Placeholder" = "navê nîşanê"; +"Scene.Register.Input.Email.Placeholder" = "e-name"; +"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Tu çima dixwazî beşdar bibî?"; +"Scene.Register.Input.Password.Hint" = "Şîfreya we herî kêm heşt tîpan hewce dike"; +"Scene.Register.Input.Password.Placeholder" = "şîfre"; +"Scene.Register.Input.Username.DuplicatePrompt" = "Navê vê bikarhêner tê girtin."; +"Scene.Register.Input.Username.Placeholder" = "navê bikarhêner"; +"Scene.Register.Title" = "Ji me re hinekî qala xwe bike."; +"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?"; +"Scene.Report.Content2" = "Is there anything the moderators should know about this report?"; +"Scene.Report.Send" = "Ragihandinê bişîne"; +"Scene.Report.SkipToSend" = "Bêyî şirove bişîne"; +"Scene.Report.Step1" = "Gav 1 ji 2"; +"Scene.Report.Step2" = "Gav 2 ji 2"; +"Scene.Report.TextPlaceholder" = "Type or paste additional comments"; +"Scene.Report.Title" = "%@ ragihîne"; +"Scene.Search.Recommend.Accounts.Description" = "Dibe ku tu bixwazî van hesaban bişopînî"; +"Scene.Search.Recommend.Accounts.Follow" = "Bişopîne"; +"Scene.Search.Recommend.Accounts.Title" = "Hesabên ku hûn dikarin hez bikin"; +"Scene.Search.Recommend.ButtonText" = "Hemûyé bibîne"; +"Scene.Search.Recommend.HashTag.Description" = "Etîketên ku pir balê dikişînin"; +"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ kes diaxivin"; +"Scene.Search.Recommend.HashTag.Title" = "Trend li ser Mastodon"; +"Scene.Search.SearchBar.Cancel" = "Betal kirin"; +"Scene.Search.SearchBar.Placeholder" = "Li etîketan û bikarhêneran bigerin"; +"Scene.Search.Searching.Clear" = "Paqij bike"; +"Scene.Search.Searching.EmptyState.NoResults" = "Encam tune"; +"Scene.Search.Searching.RecentSearch" = "Lêgerînên dawî"; +"Scene.Search.Searching.Segment.All" = "Hemû"; +"Scene.Search.Searching.Segment.Hashtags" = "Etîketan"; +"Scene.Search.Searching.Segment.People" = "Mirov"; +"Scene.Search.Searching.Segment.Posts" = "Şandîyan"; +"Scene.Search.Title" = "Bigere"; +"Scene.ServerPicker.Button.Category.Academia" = "akademî"; +"Scene.ServerPicker.Button.Category.Activism" = "çalakî"; +"Scene.ServerPicker.Button.Category.All" = "Hemû"; +"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Beş: Hemû"; +"Scene.ServerPicker.Button.Category.Art" = "huner"; +"Scene.ServerPicker.Button.Category.Food" = "xwarin"; +"Scene.ServerPicker.Button.Category.Furry" = "furry"; +"Scene.ServerPicker.Button.Category.Games" = "lîsk"; +"Scene.ServerPicker.Button.Category.General" = "giştî"; +"Scene.ServerPicker.Button.Category.Journalism" = "rojnamevanî"; +"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt"; +"Scene.ServerPicker.Button.Category.Music" = "muzîk"; +"Scene.ServerPicker.Button.Category.Regional" = "herêmî"; +"Scene.ServerPicker.Button.Category.Tech" = "teknolojî"; +"Scene.ServerPicker.Button.SeeLess" = "Kêmtir bibîne"; +"Scene.ServerPicker.Button.SeeMore" = "Bêtir bibîne"; +"Scene.ServerPicker.EmptyState.BadNetwork" = "Di dema barkirina daneyan da tiştek xelet derket. Girêdana xwe ya înternetê kontrol bike."; +"Scene.ServerPicker.EmptyState.FindingServers" = "Dîtina serverên berdest..."; +"Scene.ServerPicker.EmptyState.NoResults" = "Encam nade"; +"Scene.ServerPicker.Input.Placeholder" = "Serverek bibînin an jî beşdarî ya xwe bibin..."; +"Scene.ServerPicker.Label.Category" = "KATEGORÎ"; +"Scene.ServerPicker.Label.Language" = "ZIMAN"; +"Scene.ServerPicker.Label.Users" = "BIKARHÊNER"; +"Scene.ServerPicker.Title" = "Rajekarekê hilbijêre, +Her kîjan rajekar be."; +"Scene.ServerRules.Button.Confirm" = "Ez tev dibim"; +"Scene.ServerRules.PrivacyPolicy" = "polîtîkaya nepenîtiyê"; +"Scene.ServerRules.Prompt" = "Bi berdewamî, hûn ji bo %@ di bin şertên polîtîkaya xizmet û nepenîtiyê da ne."; +"Scene.ServerRules.Subtitle" = "Ev rêzik ji aliyê rêvebirên %@ ve tên sazkirin."; +"Scene.ServerRules.TermsOfService" = "şert û mercên xizmetê"; +"Scene.ServerRules.Title" = "Hin qaîdeyên bingehîn."; +"Scene.Settings.Footer.MastodonDescription" = "Mastodon is open source software. You can report issues on GitHub at %@ (%@)"; +"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window"; +"Scene.Settings.Section.Appearance.Automatic" = "Xweber"; +"Scene.Settings.Section.Appearance.Dark" = "Her dem tarî"; +"Scene.Settings.Section.Appearance.Light" = "Her dem ronî"; +"Scene.Settings.Section.Appearance.Title" = "Xuyang"; +"Scene.Settings.Section.BoringZone.AccountSettings" = "Account Settings"; +"Scene.Settings.Section.BoringZone.Privacy" = "Privacy Policy"; +"Scene.Settings.Section.BoringZone.Terms" = "Terms of Service"; +"Scene.Settings.Section.BoringZone.Title" = "The Boring Zone"; +"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post"; +"Scene.Settings.Section.Notifications.Favorites" = "Şandiyên min hez kir"; +"Scene.Settings.Section.Notifications.Follows" = "Min şopand"; +"Scene.Settings.Section.Notifications.Mentions" = "Qale min kir"; +"Scene.Settings.Section.Notifications.Title" = "Agahdarî"; +"Scene.Settings.Section.Notifications.Trigger.Anyone" = "her kes"; +"Scene.Settings.Section.Notifications.Trigger.Follow" = "her kesê ku dişopînim"; +"Scene.Settings.Section.Notifications.Trigger.Follower" = "şopînerek"; +"Scene.Settings.Section.Notifications.Trigger.Noone" = "ne yek"; +"Scene.Settings.Section.Notifications.Trigger.Title" = "Min agahdar bike gava"; +"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars"; +"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis"; +"Scene.Settings.Section.Preference.Title" = "Hilbijarte"; +"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode"; +"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links"; +"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache"; +"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out"; +"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone"; +"Scene.Settings.Title" = "Sazkarî"; +"Scene.SuggestionAccount.FollowExplain" = "Gava tu kesekî dişopînî, tu yê şandiyê wan di serrûpelê de bibîne."; +"Scene.SuggestionAccount.Title" = "Kesên bo ku bişopînî bibîne"; +"Scene.Thread.BackTitle" = "Şandî"; +"Scene.Thread.Title" = "Post from %@"; +"Scene.Welcome.Slogan" = "Torên civakî +di destên te de."; +"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Dest bide ser bişkoja profîlê da ku di navbera gelek ajimêrann de biguherînî."; +"Scene.Wizard.NewInMastodon" = "Nû di Mastodon de"; \ No newline at end of file diff --git a/Mastodon/Resources/ku-TR.lproj/Localizable.stringsdict b/Mastodon/Resources/ku-TR.lproj/Localizable.stringsdict new file mode 100644 index 000000000..064b8bf2b --- /dev/null +++ b/Mastodon/Resources/ku-TR.lproj/Localizable.stringsdict @@ -0,0 +1,390 @@ + + + + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 agahdariya nexwendî + other + %ld agahdariyên nexwendî + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Sînorê têketinê derbas kir %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 tîp + other + %ld tîp + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Sînorê têketinê %#@character_count@ maye + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 tîp + other + %ld tîp + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + şandî + other + şandî + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 şandî + other + %ld şandî + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hezkirin + other + %ld hezkirin + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 deng + other + %ld deng + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hilbijêr + other + %ld hilbijêr + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 mirov diaxive + other + %ld mirov diaxive + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 dişopîne + other + %ld dişopîne + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 şopîner + other + %ld şopîner + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 sal berê + other + %ld sal berê + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 meh berê + other + %ld meh berê + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 roj berê + other + %ld roj berê + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 demjimêr berê + other + %ld demjimêr berê + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 çirke berê + other + %ld çirke berê + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 sal berê + other + %ld sal berê + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 roj berê + other + %ld roj berê + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 demjimêr berê + other + %ld demjimêr berê + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 xulek berê + other + %ld xulek berê + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 çirke berê + other + %ld çirke berê + + + + diff --git a/Mastodon/Resources/th.lproj/Localizable.strings b/Mastodon/Resources/th.lproj/Localizable.strings index 0c586cab3..a61b1d15f 100644 --- a/Mastodon/Resources/th.lproj/Localizable.strings +++ b/Mastodon/Resources/th.lproj/Localizable.strings @@ -133,9 +133,9 @@ "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "กำลังโหลดโพสต์ที่ขาดหายไป..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "แสดงการตอบกลับเพิ่มเติม"; "Common.Controls.Timeline.Timestamp.Now" = "ตอนนี้"; -"Scene.AccountList.AddAccount" = "Add Account"; -"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher"; -"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher"; +"Scene.AccountList.AddAccount" = "เพิ่มบัญชี"; +"Scene.AccountList.DismissAccountSwitcher" = "ปิดตัวสลับบัญชี"; +"Scene.AccountList.TabBarHint" = "โปรไฟล์ที่เลือกในปัจจุบัน: %@ แตะสองครั้งแล้วกดค้างไว้เพื่อแสดงตัวสลับบัญชี"; "Scene.Compose.Accessibility.AppendAttachment" = "เพิ่มไฟล์แนบ"; "Scene.Compose.Accessibility.AppendPoll" = "เพิ่มการสำรวจความคิดเห็น"; "Scene.Compose.Accessibility.CustomEmojiPicker" = "ตัวเลือกอีโมจิที่กำหนดเอง"; @@ -341,6 +341,6 @@ "Scene.Thread.Title" = "โพสต์จาก %@"; "Scene.Welcome.Slogan" = "ให้เครือข่ายสังคม กลับมาอยู่ในมือของคุณ"; -"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard"; -"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button."; -"Scene.Wizard.NewInMastodon" = "New in Mastodon"; \ No newline at end of file +"Scene.Wizard.AccessibilityHint" = "แตะสองครั้งเพื่อปิดตัวช่วยสร้างนี้"; +"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "สลับระหว่างหลายบัญชีโดยกดปุ่มโปรไฟล์ค้างไว้"; +"Scene.Wizard.NewInMastodon" = "มาใหม่ใน Mastodon"; \ No newline at end of file diff --git a/Mastodon/Resources/th.lproj/Localizable.stringsdict b/Mastodon/Resources/th.lproj/Localizable.stringsdict index 1d6ff10bc..8971821f6 100644 --- a/Mastodon/Resources/th.lproj/Localizable.stringsdict +++ b/Mastodon/Resources/th.lproj/Localizable.stringsdict @@ -13,7 +13,7 @@ NSStringFormatValueTypeKey ld other - %ld unread notification + %ld การแจ้งเตือนที่ยังไม่ได้อ่าน a11y.plural.count.input_limit_exceeds diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift index 2898b9b5f..4f2ece253 100644 --- a/Mastodon/Scene/Account/AccountViewController.swift +++ b/Mastodon/Scene/Account/AccountViewController.swift @@ -111,8 +111,10 @@ extension AccountListViewController { viewModel.dataSourceDidUpdate .receive(on: DispatchQueue.main) - .sink { [weak self] in + .sink { [weak self, weak presentingViewController] in guard let self = self else { return } + // the presentingViewController may deinit + guard let _ = presentingViewController else { return } self.hasLoaded = true self.panModalSetNeedsLayoutUpdate() self.panModalTransition(to: .shortForm) diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift index 743ad1dc2..722896641 100644 --- a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift @@ -9,7 +9,7 @@ import UIKit import MetaTextKit final class AddAccountTableViewCell: UITableViewCell { - + let iconImageView: UIImageView = { let image = UIImage(systemName: "plus.circle.fill")! let imageView = UIImageView(image: image) @@ -51,6 +51,28 @@ extension AddAccountTableViewCell { ]) iconImageView.setContentHuggingPriority(.defaultLow, for: .horizontal) iconImageView.setContentHuggingPriority(.defaultLow, for: .vertical) + + // layout the same placeholder UI from `AccountListTableViewCell` + let placeholderLabelContainerStackView = UIStackView() + placeholderLabelContainerStackView.axis = .vertical + placeholderLabelContainerStackView.distribution = .equalCentering + placeholderLabelContainerStackView.spacing = 2 + placeholderLabelContainerStackView.distribution = .fillProportionally + placeholderLabelContainerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(placeholderLabelContainerStackView) + NSLayoutConstraint.activate([ + placeholderLabelContainerStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + placeholderLabelContainerStackView.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 10), + contentView.bottomAnchor.constraint(equalTo: placeholderLabelContainerStackView.bottomAnchor, constant: 10), + iconImageView.heightAnchor.constraint(equalTo: placeholderLabelContainerStackView.heightAnchor, multiplier: 0.8).priority(.required - 10), + ]) + let _nameLabel = MetaLabel(style: .accountListName) + _nameLabel.configure(content: PlaintextMetaContent(string: " ")) + let _usernameLabel = MetaLabel(style: .accountListUsername) + _usernameLabel.configure(content: PlaintextMetaContent(string: " ")) + placeholderLabelContainerStackView.addArrangedSubview(_nameLabel) + placeholderLabelContainerStackView.addArrangedSubview(_usernameLabel) + placeholderLabelContainerStackView.isHidden = true titleLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(titleLabel) @@ -58,7 +80,7 @@ extension AddAccountTableViewCell { titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 10), contentView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15), - iconImageView.heightAnchor.constraint(equalTo: titleLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), + // iconImageView.heightAnchor.constraint(equalTo: titleLabel.heightAnchor, multiplier: 1.0).priority(.required - 10), titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), ]) diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift index 7492753fe..c1e7ab6a4 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -33,6 +33,7 @@ final class AutoCompleteTableViewCell: UITableViewCell { let titleLabel: MetaLabel = { let label = MetaLabel(style: .autoCompletion) + label.isUserInteractionEnabled = false return label }() diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 3f8327909..5968df428 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -413,7 +413,7 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .sink { [weak self] attachmentServices in guard let self = self else { return } - let isEnabled = attachmentServices.count < 4 + let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled self.composeToolbarView.mediaButton.isEnabled = isEnabled self.resetImagePicker() @@ -450,7 +450,7 @@ extension ComposeViewController { .receive(on: DispatchQueue.main) .sink { [weak self] characterCount in guard let self = self else { return } - let count = ComposeViewModel.composeContentLimit - characterCount + let count = self.viewModel.composeContentLimit - characterCount self.composeToolbarView.characterCountLabel.text = "\(count)" self.characterCountLabel.text = "\(count)" let font: UIFont @@ -651,7 +651,7 @@ extension ComposeViewController { } private func resetImagePicker() { - let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count) + let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.value.count) let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) photoLibraryPicker = createImagePicker(configuration: configuration) } @@ -1275,7 +1275,6 @@ extension ComposeViewController: AutoCompleteViewControllerDelegate { case .bottomLoader: return nil } - text.append(" ") return text }() guard let replacedText = _replacedText else { return } @@ -1286,6 +1285,9 @@ extension ComposeViewController: AutoCompleteViewControllerDelegate { let range = NSRange(info.toHighlightEndRange, in: text) textEditorView.textStorage.replaceCharacters(in: range, with: replacedText) + DispatchQueue.main.async { + textEditorView.textView.insertText(" ") // trigger textView delegate update + } viewModel.autoCompleteInfo.value = nil switch item { diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index 03ee911e4..7fd07bf83 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -48,15 +48,6 @@ extension ComposeViewModel { tableView.endUpdates() } .store(in: &disposeBag) - -// composeStatusPollTableViewCell.collectionViewHeightDidUpdate -// .receive(on: DispatchQueue.main) -// .sink { [weak self] _ in -// guard let _ = self else { return } -// tableView.beginUpdates() -// tableView.endUpdates() -// } -// .store(in: &disposeBag) attachmentServices .removeDuplicates() @@ -100,7 +91,7 @@ extension ComposeViewModel { for attribute in pollOptionAttributes { items.append(.pollOption(attribute: attribute)) } - if pollOptionAttributes.count < 4 { + if pollOptionAttributes.count < self.maxPollOptions { items.append(.pollOptionAppendEntry) } items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) @@ -246,7 +237,7 @@ extension ComposeViewModel: UITableViewDataSource { return } cell.statusView.headerContainerView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerIconLabel.configure(attributedString: StatusView.iconAttributedString(image: StatusView.replyIconImage)) let headerText: String = { let author = replyTo.author let name = author.displayName.isEmpty ? author.username : author.displayName diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index f91565d38..8cb54d88a 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -15,7 +15,6 @@ import MastodonSDK final class ComposeViewModel: NSObject { - static let composeContentLimit: Int = 500 var disposeBag = Set() @@ -38,6 +37,24 @@ final class ComposeViewModel: NSObject { var isViewAppeared = false // output + let instanceConfiguration: Mastodon.Entity.Instance.Configuration? + var composeContentLimit: Int { + guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } + return max(1, maxCharacters) + } + var maxMediaAttachments: Int { + guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { + return 4 + } + // FIXME: update timeline media preview UI + return min(4, max(1, maxMediaAttachments)) + // return max(1, maxMediaAttachments) + } + var maxPollOptions: Int { + guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } + return max(2, maxOptions) + } + let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() @@ -128,8 +145,12 @@ final class ComposeViewModel: NSObject { } return CurrentValueSubject(visibility) }() - self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value) + let _activeAuthentication = context.authenticationService.activeMastodonAuthentication.value + self.activeAuthentication = CurrentValueSubject(_activeAuthentication) self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + // set limit + let _instanceConfiguration = _activeAuthentication?.instance?.configuration + self.instanceConfiguration = _instanceConfiguration super.init() // end init @@ -151,7 +172,9 @@ final class ComposeViewModel: NSObject { .sorted(by: { $0.index.intValue < $1.index.intValue }) .filter { $0.id != composeAuthor?.id } for mention in mentions { - mentionAccts.append("@" + mention.acct) + let acct = "@" + mention.acct + guard !mentionAccts.contains(acct) else { continue } + mentionAccts.append(acct) } for acct in mentionAccts { UITextChecker.learnWord(acct) @@ -241,8 +264,9 @@ final class ComposeViewModel: NSObject { let isComposeContentEmpty = composeStatusAttribute.composeContent .map { ($0 ?? "").isEmpty } let isComposeContentValid = characterCount - .map { characterCount -> Bool in - return characterCount <= ComposeViewModel.composeContentLimit + .compactMap { [weak self] characterCount -> Bool in + guard let self = self else { return characterCount <= 500 } + return characterCount <= self.composeContentLimit } let isMediaEmpty = attachmentServices .map { $0.isEmpty } @@ -379,7 +403,7 @@ final class ComposeViewModel: NSObject { .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in guard let self = self else { return } - let shouldMediaDisable = isPollComposing || attachmentServices.count >= 4 + let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments let shouldPollDisable = attachmentServices.count > 0 self.isMediaToolbarButtonEnabled.value = !shouldMediaDisable @@ -453,7 +477,7 @@ extension ComposeViewModel { extension ComposeViewModel { func createNewPollOptionIfPossible() { - guard pollOptionAttributes.value.count < 4 else { return } + guard pollOptionAttributes.value.count < maxPollOptions else { return } let attribute = ComposeStatusPollItem.PollOptionAttribute() pollOptionAttributes.value = pollOptionAttributes.value + [attribute] @@ -486,7 +510,7 @@ extension ComposeViewModel { // check exclusive limit: // - up to 1 video - // - up to 4 photos + // - up to N photos func checkAttachmentPrecondition() throws { let attachmentServices = self.attachmentServices.value guard !attachmentServices.isEmpty else { return } diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 373ce1a15..6b06973a2 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -262,7 +262,7 @@ extension ComposeToolbarView { } private static func configureToolbarButtonAppearance(button: UIButton) { - button.tintColor = Asset.Colors.brandBlue.color + button.tintColor = ThemeService.tintColor button.setBackgroundImage(.placeholder(size: ComposeToolbarView.toolbarButtonSize, color: .systemFill), for: .highlighted) button.layer.masksToBounds = true button.layer.cornerRadius = 5 diff --git a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift index cb34f3ded..6f0527d55 100644 --- a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift +++ b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift @@ -45,9 +45,10 @@ final class ReplicaStatusView: UIView { return attributedString } - let headerIconLabel: UILabel = { - let label = UILabel() - label.attributedText = ReplicaStatusView.iconAttributedString(image: ReplicaStatusView.reblogIconImage) + let headerIconLabel: MetaLabel = { + let label = MetaLabel(style: .statusHeader) + let attributedString = StatusView.iconAttributedString(image: StatusView.reblogIconImage) + label.configure(attributedString: attributedString) return label }() diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index 9801d701a..72f084fad 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -24,7 +24,7 @@ class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPre let composeBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color + // barButtonItem.tintColor = Asset.Colors.brandBlue.color barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift index 23ed744fc..a601eb927 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+Diffable.swift @@ -89,7 +89,7 @@ extension HashtagTimelineViewModel { } DispatchQueue.main.async { - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + diffableDataSource.reloadData(snapshot: newSnapshot) { tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) tableView.contentOffset.y = tableView.contentOffset.y - difference.offset self.isFetchingLatestTimeline.value = false diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index fbc221c7a..6d79d0603 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -14,6 +14,7 @@ import CoreDataStack import FLEX import SwiftUI import MastodonUI +import MastodonSDK extension HomeTimelineViewController { var debugMenu: UIMenu { @@ -27,6 +28,7 @@ extension HomeTimelineViewController { moveMenu, dropMenu, miscMenu, + notificationMenu, UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in guard let self = self else { return } self.showSettings(action) @@ -175,6 +177,25 @@ extension HomeTimelineViewController { ) } + var notificationMenu: UIMenu { + return UIMenu( + title: "Notification…", + image: UIImage(systemName: "bell.badge"), + identifier: nil, + options: [], + children: [ + UIAction(title: "Profile", image: UIImage(systemName: "person.badge.plus"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showNotification(action, notificationType: .follow) + }, + UIAction(title: "Status", image: UIImage(systemName: "list.bullet.rectangle"), attributes: []) { [weak self] action in + guard let self = self else { return } + self.showNotification(action, notificationType: .mention) + }, + ] + ) + } + } extension HomeTimelineViewController { @@ -412,6 +433,63 @@ extension HomeTimelineViewController { coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) } + private func showNotification(_ sender: UIAction, notificationType: Mastodon.Entity.Notification.NotificationType) { + guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + + let alertController = UIAlertController(title: "Enter notification ID", message: nil, preferredStyle: .alert) + alertController.addTextField() + + let showAction = UIAlertAction(title: "Show", style: .default) { [weak self, weak alertController] _ in + guard let self = self else { return } + guard let textField = alertController?.textFields?.first, + let text = textField.text, + let notificationID = Int(text) + else { return } + + let pushNotification = MastodonPushNotification( + _accessToken: authenticationBox.userAuthorization.accessToken, + notificationID: notificationID, + notificationType: notificationType.rawValue, + preferredLocale: nil, + icon: nil, + title: "", + body: "" + ) + self.context.notificationService.requestRevealNotificationPublisher.send(pushNotification) + } + alertController.addAction(showAction) + + // for multiple accounts debug + let boxes = self.context.authenticationService.mastodonAuthenticationBoxes.value // already sorted + if boxes.count >= 2 { + let accessToken = boxes[1].userAuthorization.accessToken + let showForSecondaryAction = UIAlertAction(title: "Show for Secondary", style: .default) { [weak self, weak alertController] _ in + guard let self = self else { return } + guard let textField = alertController?.textFields?.first, + let text = textField.text, + let notificationID = Int(text) + else { return } + + let pushNotification = MastodonPushNotification( + _accessToken: accessToken, + notificationID: notificationID, + notificationType: notificationType.rawValue, + preferredLocale: nil, + icon: nil, + title: "", + body: "" + ) + self.context.notificationService.requestRevealNotificationPublisher.send(pushNotification) + } + alertController.addAction(showForSecondaryAction) + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addAction(cancelAction) + + self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + } + @objc private func showSettings(_ sender: UIAction) { guard let currentSetting = context.settingService.currentSetting.value else { return } let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index dbd552d97..6b5476885 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -46,14 +46,14 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media let settingBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color + barButtonItem.tintColor = ThemeService.tintColor barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() let composeBarButtonItem: UIBarButtonItem = { let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color + barButtonItem.tintColor = ThemeService.tintColor barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) return barButtonItem }() @@ -114,6 +114,24 @@ extension HomeTimelineViewController { #endif } .store(in: &disposeBag) + #if DEBUG + // long press to trigger debug menu + settingBarButtonItem.menu = debugMenu + #else + settingBarButtonItem.target = self + settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + #endif + + viewModel.displayComposeBarButtonItem + .receive(on: DispatchQueue.main) + .sink { [weak self] displayComposeBarButtonItem in + guard let self = self else { return } + self.navigationItem.rightBarButtonItem = displayComposeBarButtonItem ? self.composeBarButtonItem : nil + } + .store(in: &disposeBag) + composeBarButtonItem.target = self + composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) + navigationItem.titleView = titleView titleView.delegate = self @@ -126,18 +144,6 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) - #if DEBUG - // long press to trigger debug menu - settingBarButtonItem.menu = debugMenu - #else - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) - #endif - - navigationItem.rightBarButtonItem = composeBarButtonItem - composeBarButtonItem.target = self - composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) - tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 73d2c1739..e87cab1c1 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -119,7 +119,7 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { return } - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + diffableDataSource.reloadData(snapshot: newSnapshot) { tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) tableView.contentOffset.y = tableView.contentOffset.y - difference.offset self.isFetchingLatestTimeline.value = false diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index a3fbcbd74..c4681b40b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -30,6 +30,8 @@ final class HomeTimelineViewModel: NSObject { let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let lastAutomaticFetchTimestamp = CurrentValueSubject(nil) let scrollPositionRecord = CurrentValueSubject(nil) + let displaySettingBarButtonItem = CurrentValueSubject(true) + let displayComposeBarButtonItem = CurrentValueSubject(true) weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? weak var tableView: UITableView? @@ -70,7 +72,6 @@ final class HomeTimelineViewModel: NSObject { let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine var diffableDataSource: UITableViewDiffableDataSource? var cellFrameCache = NSCache() - let displaySettingBarButtonItem = CurrentValueSubject(true) init(context: AppContext) { self.context = context diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index 1abb35617..0718938f6 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -20,6 +20,8 @@ final class MastodonConfirmEmailViewController: UIViewController, NeedsDependenc var viewModel: MastodonConfirmEmailViewModel! + let stackView = UIStackView() + let largeTitleLabel: UILabel = { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: UIFont.systemFont(ofSize: 34, weight: .bold)) @@ -72,9 +74,10 @@ extension MastodonConfirmEmailViewController { override func viewDidLoad() { setupOnboardingAppearance() + configureTitleLabel() + configureMargin() // stackView - let stackView = UIStackView() stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 10 @@ -92,8 +95,8 @@ extension MastodonConfirmEmailViewController { stackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: view.readableContentGuide.topAnchor), - stackView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor), ]) NSLayoutConstraint.activate([ @@ -139,6 +142,38 @@ extension MastodonConfirmEmailViewController { .store(in: &self.disposeBag) } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureTitleLabel() + configureMargin() + } + +} + +extension MastodonConfirmEmailViewController { + private func configureTitleLabel() { + switch traitCollection.horizontalSizeClass { + case .regular: + navigationItem.largeTitleDisplayMode = .always + navigationItem.title = L10n.Scene.ConfirmEmail.title.replacingOccurrences(of: "\n", with: " ") + largeTitleLabel.isHidden = true + default: + navigationItem.largeTitleDisplayMode = .never + navigationItem.title = nil + largeTitleLabel.isHidden = false + } + } + + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonConfirmEmailViewController.viewEdgeMargin + stackView.layoutMargins = UIEdgeInsets(top: 18, left: margin, bottom: 23, right: margin) + default: + stackView.layoutMargins = UIEdgeInsets(top: 10, left: 0, bottom: 23, right: 0) + } + } } extension MastodonConfirmEmailViewController { diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 3da704f0b..f3570c6c5 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -29,6 +29,8 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency private var expandServerDomainSet = Set() private let emptyStateView = PickServerEmptyStateView() + private var emptyStateViewLeadingLayoutConstraint: NSLayoutConstraint! + private var emptyStateViewTrailingLayoutConstraint: NSLayoutConstraint! let tableViewTopPaddingView = UIView() // fix empty state view background display when tableView bounce scrolling var tableViewTopPaddingViewHeightLayoutConstraint: NSLayoutConstraint! @@ -52,13 +54,14 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency return tableView }() + let buttonContainer = UIView() let nextStepButton: PrimaryActionButton = { let button = PrimaryActionButton() button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal) button.translatesAutoresizingMaskIntoConstraints = false return button }() - var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint! + var buttonContainerBottomLayoutConstraint: NSLayoutConstraint! var mastodonAuthenticationController: MastodonAuthenticationController? @@ -77,6 +80,8 @@ extension MastodonPickServerViewController { setupOnboardingAppearance() defer { setupNavigationBarBackgroundView() } + configureTitleLabel() + configureMargin() #if DEBUG navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil) @@ -89,14 +94,24 @@ extension MastodonPickServerViewController { navigationItem.rightBarButtonItem?.menu = UIMenu(title: "Debug Tool", image: nil, identifier: nil, options: [], children: children) #endif - view.addSubview(nextStepButton) - nextStepButtonBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: nextStepButton.bottomAnchor, constant: 0).priority(.defaultHigh) + buttonContainer.translatesAutoresizingMaskIntoConstraints = false + buttonContainer.preservesSuperviewLayoutMargins = true + view.addSubview(buttonContainer) + buttonContainerBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor, constant: 0).priority(.defaultHigh) NSLayoutConstraint.activate([ - nextStepButton.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: MastodonPickServerViewController.actionButtonMargin), - view.readableContentGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor, constant: MastodonPickServerViewController.actionButtonMargin), + buttonContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + buttonContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: buttonContainer.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight), + buttonContainerBottomLayoutConstraint, + ]) + + view.addSubview(nextStepButton) + NSLayoutConstraint.activate([ + nextStepButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), + nextStepButton.leadingAnchor.constraint(equalTo: buttonContainer.layoutMarginsGuide.leadingAnchor), + buttonContainer.layoutMarginsGuide.trailingAnchor.constraint(equalTo: nextStepButton.trailingAnchor), + nextStepButton.bottomAnchor.constraint(equalTo: buttonContainer.bottomAnchor), nextStepButton.heightAnchor.constraint(equalToConstant: MastodonPickServerViewController.actionButtonHeight).priority(.defaultHigh), - view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualTo: nextStepButton.bottomAnchor, constant: WelcomeViewController.viewBottomPaddingHeight), - nextStepButtonBottomLayoutConstraint, ]) // fix AutoLayout warning when observe before view appear @@ -127,16 +142,18 @@ extension MastodonPickServerViewController { tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - nextStepButton.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 7), + buttonContainer.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 7), ]) emptyStateView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(emptyStateView) + emptyStateViewLeadingLayoutConstraint = emptyStateView.leadingAnchor.constraint(equalTo: tableView.leadingAnchor) + emptyStateViewTrailingLayoutConstraint = tableView.trailingAnchor.constraint(equalTo: emptyStateView.trailingAnchor) NSLayoutConstraint.activate([ emptyStateView.topAnchor.constraint(equalTo: view.topAnchor), - emptyStateView.leadingAnchor.constraint(equalTo: tableView.readableContentGuide.leadingAnchor), - emptyStateView.trailingAnchor.constraint(equalTo: tableView.readableContentGuide.trailingAnchor), - nextStepButton.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21), + emptyStateViewLeadingLayoutConstraint, + emptyStateViewTrailingLayoutConstraint, + buttonContainer.topAnchor.constraint(equalTo: emptyStateView.bottomAnchor, constant: 21), ]) view.sendSubviewToBack(emptyStateView) @@ -154,18 +171,18 @@ extension MastodonPickServerViewController { // guard external keyboard connected guard isShow, state == .dock, GCKeyboard.coalesced != nil else { - self.nextStepButtonBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight + self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight return } let externalKeyboardToolbarHeight = self.view.frame.maxY - endFrame.minY guard externalKeyboardToolbarHeight > 0 else { - self.nextStepButtonBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight + self.buttonContainerBottomLayoutConstraint.constant = WelcomeViewController.viewBottomPaddingHeight return } UIView.animate(withDuration: 0.3) { - self.nextStepButtonBottomLayoutConstraint.constant = externalKeyboardToolbarHeight + 16 + self.buttonContainerBottomLayoutConstraint.constant = externalKeyboardToolbarHeight + 16 self.view.layoutIfNeeded() } } @@ -274,9 +291,34 @@ extension MastodonPickServerViewController { viewModel.viewWillAppear.send() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + setupNavigationBarAppearance() + updateEmptyStateViewLayout() + configureTitleLabel() + configureMargin() + } } +extension MastodonPickServerViewController { + private func configureTitleLabel() { + guard UIDevice.current.userInterfaceIdiom == .pad else { + return + } + + switch traitCollection.horizontalSizeClass { + case .regular: + navigationItem.largeTitleDisplayMode = .always + navigationItem.title = L10n.Scene.ServerPicker.title.replacingOccurrences(of: "\n", with: " ") + default: + navigationItem.largeTitleDisplayMode = .never + navigationItem.title = nil + } + } +} + extension MastodonPickServerViewController { @objc @@ -426,43 +468,6 @@ extension MastodonPickServerViewController: UITableViewDelegate { } } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let headerView = UIView() - headerView.backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - return headerView - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - guard let diffableDataSource = viewModel.diffableDataSource else { - return .leastNonzeroMagnitude - } - let sections = diffableDataSource.snapshot().sectionIdentifiers - let section = sections[section] - switch section { - case .header: - return 20 - case .category: - // Since category view has a blur shadow effect, its height need to be large than the actual height, - // Thus we reduce the section header's height by 10, and make the category cell height 60+20(10 inset for top and bottom) - return 10 - case .search: - // Same reason as above - return 10 - case .servers: - return .leastNonzeroMagnitude - } - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - let footerView = UIView() - footerView.backgroundColor = .yellow - return footerView - } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return .leastNonzeroMagnitude - } - func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { guard let diffableDataSource = viewModel.diffableDataSource else { return nil } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return nil } @@ -521,6 +526,26 @@ extension MastodonPickServerViewController { let rectInTableView = tableView.rectForRow(at: indexPath) emptyStateView.topPaddingViewTopLayoutConstraint.constant = rectInTableView.maxY + + switch traitCollection.horizontalSizeClass { + case .regular: + emptyStateViewLeadingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin + emptyStateViewTrailingLayoutConstraint.constant = MastodonPickServerViewController.viewEdgeMargin + default: + let margin = tableView.layoutMarginsGuide.layoutFrame.origin.x + emptyStateViewLeadingLayoutConstraint.constant = margin + emptyStateViewTrailingLayoutConstraint.constant = margin + } + } + + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + buttonContainer.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + buttonContainer.layoutMargins = .zero + } } } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift index 8207ccdb9..659317752 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCategoriesCell.swift @@ -56,12 +56,13 @@ extension PickServerCategoriesCell { private func _init() { selectionStyle = .none backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color + configureMargin() metricView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(metricView) NSLayoutConstraint.activate([ - metricView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - metricView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + metricView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + metricView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), metricView.topAnchor.constraint(equalTo: contentView.topAnchor), metricView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), metricView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), @@ -71,14 +72,20 @@ extension PickServerCategoriesCell { NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), - collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + collectionView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + contentView.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 20), collectionView.heightAnchor.constraint(equalToConstant: 80).priority(.defaultHigh), ]) collectionView.delegate = self } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureMargin() + } + override func layoutSubviews() { super.layoutSubviews() @@ -87,6 +94,18 @@ extension PickServerCategoriesCell { } +extension PickServerCategoriesCell { + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + contentView.layoutMargins = .zero + } + } +} + // MARK: - UICollectionViewDelegateFlowLayout extension PickServerCategoriesCell: UICollectionViewDelegateFlowLayout { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift index ee2471878..2f60a5206 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerCell.swift @@ -198,6 +198,7 @@ extension PickServerCell { private func _init() { selectionStyle = .none backgroundColor = .clear + configureMargin() contentView.addSubview(containerView) containerView.addSubview(domainLabel) @@ -229,8 +230,8 @@ extension PickServerCell { NSLayoutConstraint.activate([ // Set background view containerView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), // Set bottom separator @@ -291,6 +292,12 @@ extension PickServerCell { expandButton.addTarget(self, action: #selector(expandButtonDidPressed(_:)), for: .touchUpInside) } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureMargin() + } + private func makeVerticalInfoStackView(arrangedView: UIView...) -> UIStackView { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false @@ -318,6 +325,18 @@ extension PickServerCell { } } +extension PickServerCell { + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + contentView.layoutMargins = .zero + } + } +} + extension PickServerCell { enum ExpandMode { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift index 1b8264ec3..945ecac6a 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -37,14 +37,16 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { override func _init() { super._init() + configureMargin() + contentView.addSubview(containerView) contentView.addSubview(seperator) NSLayoutConstraint.activate([ // Set background view containerView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - contentView.readableContentGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 1), // Set bottom separator @@ -67,6 +69,24 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { activityIndicatorView.isHidden = false startAnimating() } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureMargin() + } +} + +extension PickServerLoaderTableViewCell { + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + contentView.layoutMargins = .zero + } + } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift index fa3e3ae27..0a64103d2 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift @@ -109,6 +109,7 @@ extension PickServerSearchCell { private func _init() { selectionStyle = .none backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color + configureMargin() searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) searchTextField.delegate = self @@ -118,9 +119,9 @@ extension PickServerSearchCell { contentView.addSubview(searchTextField) NSLayoutConstraint.activate([ - bgView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + bgView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), bgView.topAnchor.constraint(equalTo: contentView.topAnchor), - bgView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + bgView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), bgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textFieldBgView.leadingAnchor.constraint(equalTo: bgView.leadingAnchor, constant: 14), @@ -134,6 +135,24 @@ extension PickServerSearchCell { textFieldBgView.bottomAnchor.constraint(equalTo: searchTextField.bottomAnchor, constant: 4), ]) } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureMargin() + } +} + +extension PickServerSearchCell { + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + contentView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + contentView.layoutMargins = .zero + } + } } extension PickServerSearchCell { diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift index 682ebbf30..f0d78eb41 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerTitleCell.swift @@ -20,6 +20,8 @@ final class PickServerTitleCell: UITableViewCell { return label }() + var containerHeightLayoutConstraint: NSLayoutConstraint! + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() @@ -36,13 +38,45 @@ extension PickServerTitleCell { private func _init() { selectionStyle = .none backgroundColor = Asset.Theme.Mastodon.systemGroupedBackground.color - - contentView.addSubview(titleLabel) + + let container = UIStackView() + container.axis = .vertical + container.translatesAutoresizingMaskIntoConstraints = false + containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: .leastNonzeroMagnitude) + contentView.addSubview(container) NSLayoutConstraint.activate([ - titleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - contentView.readableContentGuide.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), - titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), - titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + container.topAnchor.constraint(equalTo: contentView.topAnchor), + container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + + container.addArrangedSubview(titleLabel) + + configureTitleLabelDisplay() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureTitleLabelDisplay() + } +} + +extension PickServerTitleCell { + private func configureTitleLabelDisplay() { + guard traitCollection.userInterfaceIdiom == .pad else { + titleLabel.isHidden = false + return + } + + switch traitCollection.horizontalSizeClass { + case .regular: + titleLabel.isHidden = true + containerHeightLayoutConstraint.isActive = true + default: + titleLabel.isHidden = false + containerHeightLayoutConstraint.isActive = false + } } } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index ffef3d872..b86c46745 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -61,6 +61,8 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O return scrollview }() + let stackView = UIStackView() + let largeTitleLabel: UILabel = { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) @@ -287,7 +289,11 @@ extension MastodonRegisterViewController { super.viewDidLoad() setupOnboardingAppearance() - defer { setupNavigationBarBackgroundView() } + configureTitleLabel() + defer { + setupNavigationBarBackgroundView() + configureFormLayout() + } avatarButton.menu = createMediaContextMenu() avatarButton.showsMenuAsPrimaryAction = true @@ -307,7 +313,6 @@ extension MastodonRegisterViewController { tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler)) // stackview - let stackView = UIStackView() stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 40 @@ -315,17 +320,24 @@ extension MastodonRegisterViewController { stackView.isLayoutMarginsRelativeArrangement = true stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(avatarView) - stackView.addArrangedSubview(usernameTextField) - stackView.addArrangedSubview(displayNameTextField) - stackView.addArrangedSubview(emailTextField) - stackView.addArrangedSubview(passwordTextField) - stackView.addArrangedSubview(passwordCheckLabel) + + let formTableStackView = UIStackView() + stackView.addArrangedSubview(formTableStackView) + formTableStackView.axis = .vertical + formTableStackView.distribution = .fill + formTableStackView.spacing = 40 + + formTableStackView.addArrangedSubview(usernameTextField) + formTableStackView.addArrangedSubview(displayNameTextField) + formTableStackView.addArrangedSubview(emailTextField) + formTableStackView.addArrangedSubview(passwordTextField) + formTableStackView.addArrangedSubview(passwordCheckLabel) if viewModel.approvalRequired { - stackView.addArrangedSubview(reasonTextField) + formTableStackView.addArrangedSubview(reasonTextField) } usernameErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - stackView.addSubview(usernameErrorPromptLabel) + formTableStackView.addSubview(usernameErrorPromptLabel) NSLayoutConstraint.activate([ usernameErrorPromptLabel.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 6), usernameErrorPromptLabel.leadingAnchor.constraint(equalTo: usernameTextField.leadingAnchor), @@ -333,7 +345,7 @@ extension MastodonRegisterViewController { ]) emailErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - stackView.addSubview(emailErrorPromptLabel) + formTableStackView.addSubview(emailErrorPromptLabel) NSLayoutConstraint.activate([ emailErrorPromptLabel.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 6), emailErrorPromptLabel.leadingAnchor.constraint(equalTo: emailTextField.leadingAnchor), @@ -341,7 +353,7 @@ extension MastodonRegisterViewController { ]) passwordErrorPromptLabel.translatesAutoresizingMaskIntoConstraints = false - stackView.addSubview(passwordErrorPromptLabel) + formTableStackView.addSubview(passwordErrorPromptLabel) NSLayoutConstraint.activate([ passwordErrorPromptLabel.topAnchor.constraint(equalTo: passwordCheckLabel.bottomAnchor, constant: 2), passwordErrorPromptLabel.leadingAnchor.constraint(equalTo: passwordTextField.leadingAnchor), @@ -373,12 +385,14 @@ extension MastodonRegisterViewController { avatarView.translatesAutoresizingMaskIntoConstraints = false avatarView.addSubview(avatarButton) NSLayoutConstraint.activate([ - avatarView.heightAnchor.constraint(equalToConstant: 90).priority(.defaultHigh), + avatarView.heightAnchor.constraint(equalToConstant: 92).priority(.required - 1), ]) avatarButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - avatarButton.heightAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), - avatarButton.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh), + avatarButton.heightAnchor.constraint(equalToConstant: 92).priority(.required - 1), + avatarButton.widthAnchor.constraint(equalToConstant: 92).priority(.required - 1), + avatarButton.leadingAnchor.constraint(greaterThanOrEqualTo: avatarView.leadingAnchor).priority(.required - 1), + avatarView.trailingAnchor.constraint(greaterThanOrEqualTo: avatarButton.trailingAnchor).priority(.required - 1), avatarButton.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor), avatarButton.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor), ]) @@ -392,15 +406,15 @@ extension MastodonRegisterViewController { // textfield NSLayoutConstraint.activate([ - usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), - passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.defaultHigh), + usernameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), + displayNameTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), + emailTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), + passwordTextField.heightAnchor.constraint(equalToConstant: 50).priority(.required - 1), ]) // password - stackView.setCustomSpacing(6, after: passwordTextField) - stackView.setCustomSpacing(32, after: passwordCheckLabel) + formTableStackView.setCustomSpacing(6, after: passwordTextField) + formTableStackView.setCustomSpacing(32, after: passwordCheckLabel) // return if viewModel.approvalRequired { @@ -410,16 +424,22 @@ extension MastodonRegisterViewController { } // button - stackView.addArrangedSubview(buttonContainer) + formTableStackView.addArrangedSubview(buttonContainer) signUpButton.translatesAutoresizingMaskIntoConstraints = false buttonContainer.addSubview(signUpButton) NSLayoutConstraint.activate([ signUpButton.topAnchor.constraint(equalTo: buttonContainer.topAnchor), - signUpButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor, constant: MastodonRegisterViewController.actionButtonMargin), - buttonContainer.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor, constant: MastodonRegisterViewController.actionButtonMargin), + signUpButton.leadingAnchor.constraint(equalTo: buttonContainer.leadingAnchor), + buttonContainer.trailingAnchor.constraint(equalTo: signUpButton.trailingAnchor), buttonContainer.bottomAnchor.constraint(equalTo: signUpButton.bottomAnchor), - signUpButton.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.defaultHigh), + signUpButton.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.required - 1), + buttonContainer.heightAnchor.constraint(equalToConstant: MastodonRegisterViewController.actionButtonHeight).priority(.required - 1), ]) + signUpButton.setContentHuggingPriority(.defaultLow, for: .horizontal) + signUpButton.setContentHuggingPriority(.defaultLow, for: .vertical) + signUpButton.setContentCompressionResistancePriority(.required - 1, for: .vertical) + signUpButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + buttonContainer.setContentCompressionResistancePriority(.required - 1, for: .vertical) Publishers.CombineLatest( KeyboardResponderService.shared.state.eraseToAnyPublisher(), @@ -645,6 +665,12 @@ extension MastodonRegisterViewController { plusIconImageView.layer.masksToBounds = true } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + configureTitleLabel() + configureFormLayout() + } } extension MastodonRegisterViewController: UITextFieldDelegate { @@ -714,7 +740,7 @@ extension MastodonRegisterViewController: UITextFieldDelegate { textField.layer.shadowRadius = 2.0 textField.layer.shadowOffset = CGSize.zero textField.layer.shadowColor = color.cgColor - textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath + // textField.layer.shadowPath = UIBezierPath(roundedRect: textField.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)).cgPath } private func setTextFieldValidAppearance(_ textField: UITextField, validateState: MastodonRegisterViewModel.ValidateState) { @@ -729,6 +755,36 @@ extension MastodonRegisterViewController: UITextFieldDelegate { } } +extension MastodonRegisterViewController { + private func configureTitleLabel() { + switch traitCollection.horizontalSizeClass { + case .regular: + navigationItem.largeTitleDisplayMode = .always + navigationItem.title = L10n.Scene.ServerPicker.title.replacingOccurrences(of: "\n", with: " ") + largeTitleLabel.isHidden = true + default: + navigationItem.largeTitleDisplayMode = .never + navigationItem.title = nil + largeTitleLabel.isHidden = false + } + } + + private func configureFormLayout() { + switch traitCollection.horizontalSizeClass { + case .regular: + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + default: + stackView.axis = .vertical + stackView.distribution = .fill + } + } + + private func configureMargin() { + + } +} + extension MastodonRegisterViewController { @objc private func tapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { view.endEditing(true) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index b9a332d05..e93d06e19 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -21,6 +21,8 @@ final class MastodonServerRulesViewController: UIViewController, NeedsDependency var viewModel: MastodonServerRulesViewModel! + let stackView = UIStackView() + let largeTitleLabel: UILabel = { let label = UILabel() label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 34, weight: .bold)) @@ -96,6 +98,8 @@ extension MastodonServerRulesViewController { super.viewDidLoad() setupOnboardingAppearance() + configureTitleLabel() + configureMargin() configTextView() defer { setupNavigationBarBackgroundView() } @@ -116,8 +120,8 @@ extension MastodonServerRulesViewController { bottomContainerView.addSubview(confirmButton) NSLayoutConstraint.activate([ bottomContainerView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: confirmButton.bottomAnchor, constant: MastodonServerRulesViewController.viewBottomPaddingHeight), - confirmButton.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin), - bottomContainerView.readableContentGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor, constant: MastodonServerRulesViewController.actionButtonMargin), + confirmButton.leadingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.leadingAnchor), + bottomContainerView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: confirmButton.trailingAnchor), confirmButton.heightAnchor.constraint(equalToConstant: MastodonServerRulesViewController.actionButtonHeight).priority(.defaultHigh), ]) @@ -125,8 +129,8 @@ extension MastodonServerRulesViewController { bottomContainerView.addSubview(bottomPromptMetaText.textView) NSLayoutConstraint.activate([ bottomPromptMetaText.textView.frameLayoutGuide.topAnchor.constraint(equalTo: bottomContainerView.topAnchor, constant: 20), - bottomPromptMetaText.textView.frameLayoutGuide.leadingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.leadingAnchor), - bottomPromptMetaText.textView.frameLayoutGuide.trailingAnchor.constraint(equalTo: bottomContainerView.readableContentGuide.trailingAnchor), + bottomPromptMetaText.textView.frameLayoutGuide.leadingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.leadingAnchor), + bottomPromptMetaText.textView.frameLayoutGuide.trailingAnchor.constraint(equalTo: bottomContainerView.layoutMarginsGuide.trailingAnchor), confirmButton.topAnchor.constraint(equalTo: bottomPromptMetaText.textView.frameLayoutGuide.bottomAnchor, constant: 20), ]) @@ -140,10 +144,10 @@ extension MastodonServerRulesViewController { scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor), ]) - let stackView = UIStackView() stackView.axis = .vertical stackView.distribution = .fill stackView.spacing = 10 + stackView.isLayoutMarginsRelativeArrangement = true stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(subtitleLabel) @@ -178,6 +182,46 @@ extension MastodonServerRulesViewController { updateScrollViewContentInset() } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + setupNavigationBarAppearance() + configureTitleLabel() + configureMargin() + } + +} + +extension MastodonServerRulesViewController { + private func configureTitleLabel() { + guard UIDevice.current.userInterfaceIdiom == .pad else { + return + } + + switch traitCollection.horizontalSizeClass { + case .regular: + navigationItem.largeTitleDisplayMode = .always + navigationItem.title = L10n.Scene.ServerRules.title.replacingOccurrences(of: "\n", with: " ") + largeTitleLabel.isHidden = true + default: + navigationItem.leftBarButtonItem = nil + navigationItem.largeTitleDisplayMode = .never + navigationItem.title = nil + largeTitleLabel.isHidden = false + } + } + + private func configureMargin() { + switch traitCollection.horizontalSizeClass { + case .regular: + let margin = MastodonPickServerViewController.viewEdgeMargin + stackView.layoutMargins = UIEdgeInsets(top: 32, left: margin, bottom: 20, right: margin) + bottomContainerView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + default: + stackView.layoutMargins = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) + bottomContainerView.layoutMargins = .zero + } + } } extension MastodonServerRulesViewController { diff --git a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift index d93c677f8..4a4d04bf6 100644 --- a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift +++ b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift @@ -24,19 +24,38 @@ extension OnboardingViewControllerAppearance { setupNavigationBarAppearance() - let backItem = UIBarButtonItem() - backItem.title = L10n.Common.Controls.Actions.back + let backItem = UIBarButtonItem( + title: L10n.Common.Controls.Actions.back, + style: .plain, + target: nil, + action: nil + ) navigationItem.backBarButtonItem = backItem } func setupNavigationBarAppearance() { // use TransparentBackground so view push / dismiss will be more visual nature // please add opaque background for status bar manually if needs - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithTransparentBackground() - navigationController?.navigationBar.standardAppearance = barAppearance - navigationController?.navigationBar.compactAppearance = barAppearance - navigationController?.navigationBar.scrollEdgeAppearance = barAppearance + + switch traitCollection.userInterfaceIdiom { + case .pad: + if traitCollection.horizontalSizeClass == .regular { + // do nothing + } else { + fallthrough + } + default: + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithTransparentBackground() + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + if #available(iOS 15.0, *) { + navigationItem.compactScrollEdgeAppearance = barAppearance + } else { + // Fallback on earlier versions + } + } } func setupNavigationBarBackgroundView() { @@ -57,3 +76,12 @@ extension OnboardingViewControllerAppearance { } } + +extension OnboardingViewControllerAppearance { + static var viewEdgeMargin: CGFloat { + guard UIDevice.current.userInterfaceIdiom == .pad else { return .zero } + + let shortEdgeWidth = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + return shortEdgeWidth * 0.17 // magic + } +} diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift index 705ae6132..a2a266f9d 100644 --- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift +++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift @@ -75,6 +75,8 @@ extension WelcomeViewController { override func viewDidLoad() { super.viewDidLoad() + navigationController?.navigationBar.prefersLargeTitles = true + navigationItem.largeTitleDisplayMode = .never view.overrideUserInterfaceStyle = .light setupOnboardingAppearance() @@ -235,7 +237,21 @@ extension WelcomeViewController { } // MARK: - OnboardingViewControllerAppearance -extension WelcomeViewController: OnboardingViewControllerAppearance { } +extension WelcomeViewController: OnboardingViewControllerAppearance { + func setupNavigationBarAppearance() { + // always transparent + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithTransparentBackground() + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + if #available(iOS 15.0, *) { + navigationItem.compactScrollEdgeAppearance = barAppearance + } else { + // Fallback on earlier versions + } + } +} // MARK: - UIAdaptivePresentationControllerDelegate extension WelcomeViewController: UIAdaptivePresentationControllerDelegate { @@ -245,7 +261,12 @@ extension WelcomeViewController: UIAdaptivePresentationControllerDelegate { // make underneath view controller alive to fix layout issue due to view life cycle return .fullScreen default: - return .pageSheet + switch traitCollection.horizontalSizeClass { + case .regular: + return .pageSheet + default: + return .fullScreen + } } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift new file mode 100644 index 000000000..627ed7772 --- /dev/null +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift @@ -0,0 +1,50 @@ +// +// FollowerListViewController+Provider.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack + +extension FollowerListViewController: UserProvider { + + func mastodonUser() -> Future { + Future { promise in + promise(.success(nil)) + } + } + + func mastodonUser(for cell: UITableViewCell?) -> Future { + Future { [weak self] promise in + guard let self = self else { return } + guard let diffableDataSource = self.viewModel.diffableDataSource else { + assertionFailure() + promise(.success(nil)) + return + } + guard let cell = cell, + let indexPath = self.tableView.indexPath(for: cell), + let item = diffableDataSource.itemIdentifier(for: indexPath) else { + promise(.success(nil)) + return + } + + let managedObjectContext = self.viewModel.userFetchedResultsController.fetchedResultsController.managedObjectContext + + switch item { + case .follower(let objectID): + managedObjectContext.perform { + let user = managedObjectContext.object(with: objectID) as? MastodonUser + promise(.success(user)) + } + case .bottomLoader, .bottomHeader: + promise(.success(nil)) + } + } + } +} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift new file mode 100644 index 000000000..428448666 --- /dev/null +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -0,0 +1,111 @@ +// +// FollowerListViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import os.log +import UIKit +import AVKit +import GameplayKit +import Combine + +final class FollowerListViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { + + var disposeBag = Set() + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var viewModel: FollowerListViewModel! + + let mediaPreviewTransitionController = MediaPreviewTransitionController() + + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.register(UserTableViewCell.self, forCellReuseIdentifier: String(describing: UserTableViewCell.self)) + tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self)) + tableView.register(TimelineFooterTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineFooterTableViewCell.self)) + tableView.rowHeight = UITableView.automaticDimension + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + return tableView + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension FollowerListViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor + ThemeService.shared.currentTheme + .receive(on: RunLoop.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.view.backgroundColor = theme.secondarySystemBackgroundColor + } + .store(in: &disposeBag) + + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + tableView.delegate = self + viewModel.setupDiffableDataSource( + for: tableView, + dependency: self + ) + // TODO: add UserTableViewCellDelegate + + // trigger user timeline loading + Publishers.CombineLatest( + viewModel.domain.removeDuplicates().eraseToAnyPublisher(), + viewModel.userID.removeDuplicates().eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.viewModel.stateMachine.enter(FollowerListViewModel.State.Reloading.self) + } + .store(in: &disposeBag) + } + +} + +// MARK: - LoadMoreConfigurableTableViewContainer +extension FollowerListViewController: LoadMoreConfigurableTableViewContainer { + typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell + typealias LoadingState = FollowerListViewModel.State.Loading + var loadMoreConfigurableTableView: UITableView { tableView } + var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.stateMachine } +} + +// MARK: - UIScrollViewDelegate +extension FollowerListViewController { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + handleScrollViewDidScroll(scrollView) + } +} + + +// MARK: - UITableViewDelegate +extension FollowerListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + handleTableView(tableView, didSelectRowAt: indexPath) + } +} + +// MARK: - UserTableViewCellDelegate +extension FollowerListViewController: UserTableViewCellDelegate { } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift new file mode 100644 index 000000000..90b9cb311 --- /dev/null +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift @@ -0,0 +1,58 @@ +// +// FollowerListViewModel+Diffable.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import UIKit + +extension FollowerListViewModel { + func setupDiffableDataSource( + for tableView: UITableView, + dependency: NeedsDependency + ) { + diffableDataSource = UserSection.tableViewDiffableDataSource( + for: tableView, + dependency: dependency, + managedObjectContext: userFetchedResultsController.fetchedResultsController.managedObjectContext + ) + + // set empty section to make update animation top-to-bottom style + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) + + // workaround to append loader wrong animation issue + snapshot.appendItems([.bottomLoader], toSection: .main) + diffableDataSource?.apply(snapshot) + + userFetchedResultsController.objectIDs.removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] objectIDs in + guard let self = self else { return } + guard let diffableDataSource = self.diffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items: [UserItem] = objectIDs.map { + UserItem.follower(objectID: $0) + } + snapshot.appendItems(items, toSection: .main) + + if let currentState = self.stateMachine.currentState { + switch currentState { + case is State.Idle, is State.Loading, is State.Fail: + snapshot.appendItems([.bottomLoader], toSection: .main) + case is State.NoMore: + break + default: + break + } + } + + diffableDataSource.apply(snapshot) + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift new file mode 100644 index 000000000..b012a59bb --- /dev/null +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -0,0 +1,196 @@ +// +// FollowerListViewModel+State.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import os.log +import Foundation +import GameplayKit +import MastodonSDK + +extension FollowerListViewModel { + class State: GKState { + weak var viewModel: FollowerListViewModel? + + init(viewModel: FollowerListViewModel) { + self.viewModel = viewModel + } + + override func didEnter(from previousState: GKState?) { + os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) + } + } +} + +extension FollowerListViewModel.State { + class Initial: FollowerListViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + guard let viewModel = viewModel else { return false } + switch stateClass { + case is Reloading.Type: + return viewModel.userID.value != nil + default: + return false + } + } + } + + class Reloading: FollowerListViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Loading.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + + // reset + viewModel.userFetchedResultsController.userIDs.value = [] + + stateMachine.enter(Loading.self) + } + } + + class Fail: FollowerListViewModel.State { + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Loading.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let _ = viewModel, let stateMachine = stateMachine else { return } + + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading 3s later…", ((#file as NSString).lastPathComponent), #line, #function) + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: retry loading", ((#file as NSString).lastPathComponent), #line, #function) + stateMachine.enter(Loading.self) + } + } + } + + class Idle: FollowerListViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Reloading.Type, is Loading.Type: + return true + default: + return false + } + } + } + + class Loading: FollowerListViewModel.State { + + var maxID: String? + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Fail.Type: + return true + case is Idle.Type: + return true + case is NoMore.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + if previousState is Reloading { + maxID = nil + } + + guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + + guard let userID = viewModel.userID.value, !userID.isEmpty else { + stateMachine.enter(Fail.self) + return + } + + guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else { + stateMachine.enter(Fail.self) + return + } + + viewModel.context.apiService.followers( + userID: userID, + maxID: maxID, + authorizationBox: activeMastodonAuthenticationBox + ) + .receive(on: DispatchQueue.main) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + stateMachine.enter(Fail.self) + case .finished: + break + } + } receiveValue: { response in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + var hasNewAppend = false + var userIDs = viewModel.userFetchedResultsController.userIDs.value + for user in response.value { + guard !userIDs.contains(user.id) else { continue } + userIDs.append(user.id) + hasNewAppend = true + } + + let maxID = response.link?.maxID + + if hasNewAppend, maxID != nil { + stateMachine.enter(Idle.self) + } else { + stateMachine.enter(NoMore.self) + } + self.maxID = maxID + viewModel.userFetchedResultsController.userIDs.value = userIDs + } + .store(in: &viewModel.disposeBag) + } // end func didEnter + } + + class NoMore: FollowerListViewModel.State { + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + switch stateClass { + case is Reloading.Type: + return true + default: + return false + } + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + guard let viewModel = viewModel, let _ = stateMachine else { return } + guard let diffableDataSource = viewModel.diffableDataSource else { + assertionFailure() + return + } + DispatchQueue.main.async { + var snapshot = diffableDataSource.snapshot() + snapshot.deleteItems([.bottomLoader]) + let header = UserItem.bottomHeader(text: "Followers from other servers are not displayed") + snapshot.appendItems([header], toSection: .main) + diffableDataSource.apply(snapshot, animatingDifferences: false) + } + } + } +} diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift new file mode 100644 index 000000000..f62441cf1 --- /dev/null +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift @@ -0,0 +1,53 @@ +// +// FollowerListViewModel.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import Foundation +import Combine +import Combine +import CoreData +import CoreDataStack +import GameplayKit +import MastodonSDK + +final class FollowerListViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + let domain: CurrentValueSubject + let userID: CurrentValueSubject + let userFetchedResultsController: UserFetchedResultsController + + // output + var diffableDataSource: UITableViewDiffableDataSource? + private(set) lazy var stateMachine: GKStateMachine = { + let stateMachine = GKStateMachine(states: [ + State.Initial(viewModel: self), + State.Reloading(viewModel: self), + State.Fail(viewModel: self), + State.Idle(viewModel: self), + State.Loading(viewModel: self), + State.NoMore(viewModel: self), + ]) + stateMachine.enter(State.Initial.self) + return stateMachine + }() + + init(context: AppContext, domain: String?, userID: String?) { + self.context = context + self.userFetchedResultsController = UserFetchedResultsController( + managedObjectContext: context.managedObjectContext, + domain: domain, + additionalTweetPredicate: nil + ) + self.domain = CurrentValueSubject(domain) + self.userID = CurrentValueSubject(userID) + // super.init() + + } +} diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 716b62307..34716dde5 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -44,10 +44,11 @@ final class ProfileHeaderViewController: UIViewController { let profileHeaderView = ProfileHeaderView() let pageSegmentedControl: UISegmentedControl = { - let segmenetedControl = UISegmentedControl(items: ["A", "B"]) - segmenetedControl.selectedSegmentIndex = 0 - return segmenetedControl + let segmentedControl = UISegmentedControl(items: ["A", "B"]) + segmentedControl.selectedSegmentIndex = 0 + return segmentedControl }() + var pageSegmentedControlLeadingLayoutConstraint: NSLayoutConstraint! private var isBannerPinned = false private var bottomShadowAlpha: CGFloat = 0.0 @@ -118,9 +119,10 @@ extension ProfileHeaderViewController { pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pageSegmentedControl) + pageSegmentedControlLeadingLayoutConstraint = pageSegmentedControl.leadingAnchor.constraint(equalTo: view.leadingAnchor) NSLayoutConstraint.activate([ pageSegmentedControl.topAnchor.constraint(equalTo: profileHeaderView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight), - pageSegmentedControl.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor), + pageSegmentedControlLeadingLayoutConstraint, // Fix iPad layout issue pageSegmentedControl.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor), view.bottomAnchor.constraint(equalTo: pageSegmentedControl.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight), pageSegmentedControl.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.segmentedControlHeight).priority(.defaultHigh), @@ -133,10 +135,10 @@ extension ProfileHeaderViewController { viewModel.isTitleViewContentOffsetSet.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSetted in + .sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSet in guard let self = self else { return } - self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0 - self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0 + self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0 + self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0 } .store(in: &disposeBag) @@ -283,6 +285,13 @@ extension ProfileHeaderViewController { setupBottomShadow() } + override func viewLayoutMarginsDidChange() { + super.viewLayoutMarginsDidChange() + + let margin = view.frame.maxX - view.readableContentGuide.layoutFrame.maxX + pageSegmentedControlLeadingLayoutConstraint.constant = margin + } + } extension ProfileHeaderViewController { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 90f2e7a11..016b31a1e 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -17,9 +17,7 @@ protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) } final class ProfileHeaderView: UIView { @@ -443,6 +441,7 @@ extension ProfileHeaderView { bringSubviewToFront(bannerContainerView) bringSubviewToFront(nameContainerStackView) + statusDashboardView.delegate = self bioMetaText.textView.delegate = self bioMetaText.textView.linkDelegate = self @@ -549,19 +548,9 @@ extension ProfileHeaderView: MetaTextViewDelegate { // MARK: - ProfileStatusDashboardViewDelegate extension ProfileHeaderView: ProfileStatusDashboardViewDelegate { - - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { - delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, postDashboardMeterViewDidPressed: dashboardMeterView) + func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) { + delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, dashboardMeterViewDidPressed: dashboardMeterView, meter: meter) } - - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { - delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followingDashboardMeterViewDidPressed: dashboardMeterView) - } - - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { - delegate?.profileHeaderView(self, profileStatusDashboardView: dashboardView, followersDashboardMeterViewDidPressed: dashboardMeterView) - } - } // MARK: - AvatarConfigurableView diff --git a/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift index 0360421a8..c21703c08 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileStatusDashboardView.swift @@ -9,9 +9,7 @@ import os.log import UIKit protocol ProfileStatusDashboardViewDelegate: AnyObject { - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) - func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) + func profileStatusDashboardView(_ dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) } final class ProfileStatusDashboardView: UIView { @@ -34,6 +32,14 @@ final class ProfileStatusDashboardView: UIView { } +extension ProfileStatusDashboardView { + enum Meter: Hashable { + case post + case following + case follower + } +} + extension ProfileStatusDashboardView { private func _init() { let containerStackView = UIStackView() @@ -67,7 +73,6 @@ extension ProfileStatusDashboardView { tapGestureRecognizer.addTarget(self, action: #selector(ProfileStatusDashboardView.tapGestureRecognizerHandler(_:))) meterView.addGestureRecognizer(tapGestureRecognizer) } - } } @@ -78,12 +83,15 @@ extension ProfileStatusDashboardView { assertionFailure() return } - if sourceView === postDashboardMeterView { - delegate?.profileStatusDashboardView(self, postDashboardMeterViewDidPressed: sourceView) - } else if sourceView === followingDashboardMeterView { - delegate?.profileStatusDashboardView(self, followingDashboardMeterViewDidPressed: sourceView) - } else if sourceView === followersDashboardMeterView { - delegate?.profileStatusDashboardView(self, followersDashboardMeterViewDidPressed: sourceView) + switch sourceView { + case postDashboardMeterView: + delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .post) + case followingDashboardMeterView: + delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .following) + case followersDashboardMeterView: + delegate?.profileStatusDashboardView(self, dashboardMeterViewDidPressed: sourceView, meter: .follower) + default: + assertionFailure() } } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 434836ab4..04d582315 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -517,6 +517,7 @@ extension ProfileViewController { .assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.note) .store(in: &disposeBag) viewModel.statusesCount + .receive(on: DispatchQueue.main) .sink { [weak self] count in guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" @@ -526,6 +527,7 @@ extension ProfileViewController { } .store(in: &disposeBag) viewModel.followingCount + .receive(on: DispatchQueue.main) .sink { [weak self] count in guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" @@ -535,6 +537,7 @@ extension ProfileViewController { } .store(in: &disposeBag) viewModel.followersCount + .receive(on: DispatchQueue.main) .sink { [weak self] count in guard let self = self else { return } let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-" @@ -766,7 +769,6 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate { // MARK: - ProfileHeaderViewDelegate extension ProfileViewController: ProfileHeaderViewDelegate { - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarImageViewDidPressed imageView: UIImageView) { guard let mastodonUser = viewModel.mastodonUser.value else { return } guard let avatar = imageView.image else { return } @@ -979,15 +981,29 @@ extension ProfileViewController: ProfileHeaderViewDelegate { } } - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { - - } - - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) { - - } - - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) { + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView dashboardView: ProfileStatusDashboardView, dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView, meter: ProfileStatusDashboardView.Meter) { + switch meter { + case .post: + // do nothing + break + case .follower: + guard let domain = viewModel.domain.value, + let userID = viewModel.userID.value + else { return } + let followerListViewModel = FollowerListViewModel( + context: context, + domain: domain, + userID: userID + ) + coordinator.present( + scene: .follower(viewModel: followerListViewModel), + from: self, + transition: .show + ) + case .following: + // TODO: + break + } } } diff --git a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift index 153f50998..ef04d5811 100644 --- a/Mastodon/Scene/Profile/RemoteProfileViewModel.swift +++ b/Mastodon/Scene/Profile/RemoteProfileViewModel.swift @@ -7,6 +7,7 @@ import os.log import Foundation +import Combine import CoreDataStack import MastodonSDK @@ -49,4 +50,51 @@ final class RemoteProfileViewModel: ProfileViewModel { .store(in: &disposeBag) } + init(context: AppContext, notificationID: Mastodon.Entity.Notification.ID) { + super.init(context: context, optionalMastodonUser: nil) + + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let domain = activeMastodonAuthenticationBox.domain + let authorization = activeMastodonAuthenticationBox.userAuthorization + + context.apiService.notification( + notificationID: notificationID, + mastodonAuthenticationBox: activeMastodonAuthenticationBox + ) + .compactMap { [weak self] response -> AnyPublisher, Error>? in + let userID = response.value.account.id + // TODO: use .account directly + return context.apiService.accountInfo( + domain: domain, + userID: userID, + authorization: authorization + ) + } + .switchToLatest() + .retry(3) + .sink { completion in + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote notification %s user fetch failed: %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: remote notification %s user fetched", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let managedObjectContext = context.managedObjectContext + let request = MastodonUser.sortedFetchRequest + request.fetchLimit = 1 + request.predicate = MastodonUser.predicate(domain: domain, id: response.value.id) + guard let mastodonUser = managedObjectContext.safeFetch(request).first else { + assertionFailure() + return + } + self.mastodonUser.value = mastodonUser + } + .store(in: &disposeBag) + } + } diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift index 16dcbbeb6..23630741f 100644 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift @@ -21,6 +21,19 @@ final class ProfilePagingViewController: TabmanViewController { // MARK: - PageboyViewControllerDelegate + override func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) { + super.pageboyViewController(pageboyViewController, didCancelScrollToPageAt: index, returnToPageAt: previousIndex) + + // Fix the SDK bug for table view get row selected during swipe but cancel paging + guard previousIndex < viewModel.viewControllers.count else { return } + let viewController = viewModel.viewControllers[previousIndex] + + if let tableView = viewController.scrollView as? UITableView { + for cell in tableView.visibleCells { + cell.setHighlighted(false, animated: false) + } + } + } override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated) diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index f3803da01..42e9376cf 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -12,7 +12,6 @@ import Combine import CoreDataStack import GameplayKit -// TODO: adopt MediaPreviewableViewController final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift index c3b1a3d4b..6d6ecbd34 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel.swift @@ -102,7 +102,7 @@ class PublicTimelineViewModel: NSObject { return } - diffableDataSource.apply(snapshot, animatingDifferences: false) { + diffableDataSource.reloadData(snapshot: snapshot) { tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false) tableView.contentOffset.y = tableView.contentOffset.y - difference.offset self.isFetchingLatestTimeline.value = false diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift index efaa533e1..6a7161c91 100644 --- a/Mastodon/Scene/Report/ReportViewController.swift +++ b/Mastodon/Scene/Report/ReportViewController.swift @@ -251,7 +251,7 @@ class ReportViewController: UIViewController, NeedsDependency { = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel, target: self, action: #selector(doneButtonDidClick)) - navigationItem.rightBarButtonItem?.tintColor = Asset.Colors.brandBlue.color + navigationItem.rightBarButtonItem?.tintColor = ThemeService.tintColor // fetch old mastodon user let beReportedUser: MastodonUser? = { diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift new file mode 100644 index 000000000..850b1429f --- /dev/null +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -0,0 +1,107 @@ +// +// ContentSplitViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-28. +// + +import os.log +import UIKit +import Combine +import CoreDataStack + +protocol ContentSplitViewControllerDelegate: AnyObject { + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) +} + +final class ContentSplitViewController: UIViewController, NeedsDependency { + + var disposeBag = Set() + + static let sidebarWidth: CGFloat = 89 + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + weak var delegate: ContentSplitViewControllerDelegate? + + private(set) lazy var sidebarViewController: SidebarViewController = { + let sidebarViewController = SidebarViewController() + sidebarViewController.context = context + sidebarViewController.coordinator = coordinator + sidebarViewController.viewModel = SidebarViewModel(context: context) + sidebarViewController.delegate = self + return sidebarViewController + }() + + @Published var currentSupplementaryTab: MainTabBarController.Tab = .home + private(set) lazy var mainTabBarController: MainTabBarController = { + let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) { + homeTimelineViewController.viewModel.displayComposeBarButtonItem.value = false + homeTimelineViewController.viewModel.displaySettingBarButtonItem.value = false + } + return mainTabBarController + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ContentSplitViewController { + override func viewDidLoad() { + super.viewDidLoad() + + navigationController?.setNavigationBarHidden(true, animated: false) + + addChild(sidebarViewController) + sidebarViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(sidebarViewController.view) + sidebarViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + sidebarViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + sidebarViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + sidebarViewController.view.widthAnchor.constraint(equalToConstant: ContentSplitViewController.sidebarWidth), + ]) + + addChild(mainTabBarController) + mainTabBarController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(mainTabBarController.view) + sidebarViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + mainTabBarController.view.topAnchor.constraint(equalTo: view.topAnchor), + mainTabBarController.view.leadingAnchor.constraint(equalTo: sidebarViewController.view.trailingAnchor, constant: UIView.separatorLineHeight(of: view)), + mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainTabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + $currentSupplementaryTab + .removeDuplicates() + .sink(receiveValue: { [weak self] tab in + guard let self = self else { return } + self.mainTabBarController.selectedIndex = tab.rawValue + self.mainTabBarController.currentTab.value = tab + }) + .store(in: &disposeBag) + } +} + +// MARK: - SidebarViewControllerDelegate +extension ContentSplitViewController: SidebarViewControllerDelegate { + + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { + delegate?.contentSplitViewController(self, sidebarViewController: sidebarViewController, didSelectTab: tab) + } + + func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) { + guard case let .tab(tab) = item, tab == .me else { return } + + let accountListViewController = coordinator.present(scene: .accountList, from: nil, transition: .popover(sourceView: sourceView)) as! AccountListViewController + accountListViewController.dragIndicatorView.barView.isHidden = true + accountListViewController.preferredContentSize = CGSize(width: 300, height: 320) + } + +} diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift index 8f3f2eea4..b69a6b786 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift @@ -70,6 +70,9 @@ extension MainTabBarController.Wizard { func setup(in view: UIView) { assert(delegate != nil, "need set delegate before use") + + guard !items.isEmpty else { return } + backgroundView.frame = view.bounds backgroundView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(backgroundView) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 1681b6171..d34c85531 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -226,16 +226,6 @@ extension MainTabBarController { } .store(in: &disposeBag) - context.notificationService.requestRevealNotificationPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] notificationID in - guard let self = self else { return } - self.coordinator.switchToTabBar(tab: .notification) - let threadViewModel = RemoteThreadViewModel(context: self.context, notificationID: notificationID) - self.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) - } - .store(in: &disposeBag) - layoutAvatarButton() context.authenticationService.activeMastodonAuthentication .receive(on: DispatchQueue.main) @@ -317,7 +307,7 @@ extension MainTabBarController { switch tab { case .me: - coordinator.present(scene: .accountList, from: nil, transition: .panModal) + coordinator.present(scene: .accountList, from: self, transition: .panModal) default: break } @@ -353,7 +343,6 @@ extension MainTabBarController { self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical) self.avatarButton.isUserInteractionEnabled = false } - } extension MainTabBarController { diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index 2e8beef9e..7c03287f1 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -14,57 +14,53 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { var disposeBag = Set() + static let sidebarWidth: CGFloat = 89 + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - private(set) lazy var sidebarViewController: SidebarViewController = { - let sidebarViewController = SidebarViewController() - sidebarViewController.context = context - sidebarViewController.coordinator = coordinator - sidebarViewController.viewModel = SidebarViewModel(context: context) - sidebarViewController.delegate = self - return sidebarViewController - }() - - var currentSupplementaryTab: MainTabBarController.Tab = .home - private(set) lazy var supplementaryViewControllers: [UIViewController] = { - let viewControllers = MainTabBarController.Tab.allCases.map { tab in - tab.viewController(context: context, coordinator: coordinator) - } - for viewController in viewControllers { - guard let navigationController = viewController as? UINavigationController else { - assertionFailure() - continue - } - if let homeViewController = navigationController.topViewController as? HomeTimelineViewController { - homeViewController.viewModel.displaySettingBarButtonItem.value = false - } - } - return viewControllers + private(set) lazy var contentSplitViewController: ContentSplitViewController = { + let contentSplitViewController = ContentSplitViewController() + contentSplitViewController.context = context + contentSplitViewController.coordinator = coordinator + contentSplitViewController.delegate = self + return contentSplitViewController }() - private(set) lazy var mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + private(set) lazy var searchViewController: SearchViewController = { + let searchViewController = SearchViewController() + searchViewController.context = context + searchViewController.coordinator = coordinator + return searchViewController + }() + + lazy var compactMainTabBarViewController = MainTabBarController(context: context, coordinator: coordinator) + + let separatorLine = UIView.separatorLine init(context: AppContext, coordinator: SceneCoordinator) { self.context = context self.coordinator = coordinator - super.init(style: .tripleColumn) + super.init(style: .doubleColumn) + primaryEdge = .trailing primaryBackgroundStyle = .sidebar - preferredDisplayMode = .oneBesideSecondary + preferredDisplayMode = .twoBesideSecondary preferredSplitBehavior = .tile delegate = self + // disable edge swipe gesture + presentsWithGesture = false + if #available(iOS 14.5, *) { - displayModeButtonVisibility = .always + displayModeButtonVisibility = .never } else { // Fallback on earlier versions } - setViewController(sidebarViewController, for: .primary) - setViewController(supplementaryViewControllers[0], for: .supplementary) - setViewController(SecondaryPlaceholderViewController(), for: .secondary) - setViewController(mainTabBarController, for: .compact) + setViewController(searchViewController, for: .primary) + setViewController(contentSplitViewController, for: .secondary) + setViewController(compactMainTabBarViewController, for: .compact) } required init?(coder: NSCoder) { @@ -83,16 +79,20 @@ extension RootSplitViewController { super.viewDidLoad() updateBehavior(size: view.frame.size) - - mainTabBarController.currentTab + contentSplitViewController.$currentSupplementaryTab .receive(on: DispatchQueue.main) - .sink { [weak self] tab in + .sink { [weak self] _ in guard let self = self else { return } - guard tab != self.currentSupplementaryTab else { return } - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { return } - self.currentSupplementaryTab = tab - self.setViewController(self.supplementaryViewControllers[index], for: .supplementary) - + self.updateBehavior(size: self.view.frame.size) + } + .store(in: &disposeBag) + + setupBackground(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackground(theme: theme) } .store(in: &disposeBag) } @@ -100,93 +100,97 @@ extension RootSplitViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - updateBehavior(size: size) + coordinator.animate { [weak self] context in + guard let self = self else { return } + self.updateBehavior(size: size) + } completion: { context in + // do nothing + } } private func updateBehavior(size: CGSize) { - // fix secondary too small on iPad mini issue - if size.width > 960 { - preferredDisplayMode = .oneBesideSecondary - preferredSplitBehavior = .tile - } else { - preferredDisplayMode = .oneBesideSecondary - preferredSplitBehavior = .displace + switch contentSplitViewController.currentSupplementaryTab { + case .search: + hide(.primary) + default: + if size.width > 960 { + show(.primary) + } else { + hide(.primary) + } } } + +} + +extension RootSplitViewController { + + private func setupBackground(theme: Theme) { + // this set column separator line color + view.backgroundColor = theme.separator + } } -// MARK: - SidebarViewControllerDelegate -extension RootSplitViewController: SidebarViewControllerDelegate { - - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { - - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { +// MARK: - ContentSplitViewControllerDelegate +extension RootSplitViewController: ContentSplitViewControllerDelegate { + func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { + guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { assertionFailure() return } - currentSupplementaryTab = tab - setViewController(supplementaryViewControllers[index], for: .supplementary) - } - - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) { - // self.sidebarViewController(sidebarViewController, didSelectTab: .search) - - let supplementaryViewController = viewController(for: .supplementary) - let managedObjectContext = context.managedObjectContext - managedObjectContext.perform { - let searchHistory = managedObjectContext.object(with: searchHistoryViewModel.searchHistoryObjectID) as! SearchHistory - if let account = searchHistory.account { - DispatchQueue.main.async { - let profileViewModel = CachedProfileViewModel(context: self.context, mastodonUser: account) - self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: supplementaryViewController, transition: .show) - } - } else if let hashtag = searchHistory.hashtag { - DispatchQueue.main.async { - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: self.context, hashtag: hashtag.name) - self.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: supplementaryViewController, transition: .show) - } + switch tab { + case .search: + guard let navigationController = searchViewController.navigationController else { return } + if navigationController.viewControllers.count == 1 { + searchViewController.searchBarTapPublisher.send() } else { - assertionFailure() + navigationController.popToRootViewController(animated: true) } + + default: + let previousTab = contentSplitViewController.currentSupplementaryTab + contentSplitViewController.currentSupplementaryTab = tab + + if previousTab == tab, + let navigationController = contentSplitViewController.mainTabBarController.selectedViewController as? UINavigationController + { + navigationController.popToRootViewController(animated: true) + } + } } - } // MARK: - UISplitViewControllerDelegate extension RootSplitViewController: UISplitViewControllerDelegate { + private static func transform(from: UITabBarController, to: UITabBarController) { + let sourceNavigationControllers = from.viewControllers ?? [] + let targetNavigationControllers = to.viewControllers ?? [] + + for (source, target) in zip(sourceNavigationControllers, targetNavigationControllers) { + guard let source = source as? UINavigationController, + let target = target as? UINavigationController + else { continue } + let viewControllers = source.popToRootViewController(animated: false) ?? [] + _ = target.popToRootViewController(animated: false) + target.viewControllers.append(contentsOf: viewControllers) + } + + to.selectedIndex = from.selectedIndex + } + // .regular to .compact - // move navigation stack from .supplementary & .secondary to .compact func splitViewController( _ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column ) -> UISplitViewController.Column { switch proposedTopColumn { case .compact: - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: currentSupplementaryTab) else { - assertionFailure() - break - } - mainTabBarController.selectedIndex = index - mainTabBarController.currentTab.value = currentSupplementaryTab - - guard let navigationController = mainTabBarController.selectedViewController as? UINavigationController else { break } - navigationController.popToRootViewController(animated: false) - var viewControllers = navigationController.viewControllers // init navigation stack with topMost + RootSplitViewController.transform(from: contentSplitViewController.mainTabBarController, to: compactMainTabBarViewController) + compactMainTabBarViewController.currentTab.value = contentSplitViewController.currentSupplementaryTab - if let supplementaryNavigationController = viewController(for: .supplementary) as? UINavigationController { - // append supplementary - viewControllers.append(contentsOf: supplementaryNavigationController.popToRootViewController(animated: true) ?? []) - } - if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { - // append secondary - viewControllers.append(contentsOf: secondaryNavigationController.popToRootViewController(animated: true) ?? []) - } - // set navigation stack - navigationController.setViewControllers(viewControllers, animated: false) - default: assertionFailure() } @@ -195,30 +199,26 @@ extension RootSplitViewController: UISplitViewControllerDelegate { } // .compact to .regular - // restore navigation stack to .supplementary & .secondary func splitViewController( _ svc: UISplitViewController, displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode ) -> UISplitViewController.DisplayMode { - let compactNavigationController = mainTabBarController.selectedViewController as? UINavigationController - let viewControllers = compactNavigationController?.popToRootViewController(animated: true) ?? [] + let compactNavigationController = compactMainTabBarViewController.selectedViewController as? UINavigationController + + if let topMost = compactNavigationController?.topMost, + topMost is AccountListViewController { + topMost.dismiss(animated: false, completion: nil) + } + + RootSplitViewController.transform(from: compactMainTabBarViewController, to: contentSplitViewController.mainTabBarController) - var supplementaryViewControllers: [UIViewController] = [] - var secondaryViewControllers: [UIViewController] = [] - for viewController in viewControllers { - if coordinator.secondaryStackHashValues.contains(viewController.hashValue) { - secondaryViewControllers.append(viewController) - } else { - supplementaryViewControllers.append(viewController) - } - - } - if let supplementary = viewController(for: .supplementary) as? UINavigationController { - supplementary.setViewControllers(supplementary.viewControllers + supplementaryViewControllers, animated: false) - } - if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { - secondaryNavigationController.setViewControllers(secondaryNavigationController.viewControllers + secondaryViewControllers, animated: false) + let tab = compactMainTabBarViewController.currentTab.value + if tab == .search { + contentSplitViewController.currentSupplementaryTab = .home + } else { + contentSplitViewController.currentSupplementaryTab = compactMainTabBarViewController.currentTab.value } + return proposedDisplayMode } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 69d9f55c8..7958c5080 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -12,37 +12,57 @@ import CoreDataStack protocol SidebarViewControllerDelegate: AnyObject { func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) + func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) } final class SidebarViewController: UIViewController, NeedsDependency { + let logger = Logger(subsystem: "SidebarViewController", category: "ViewController") + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() + var observations = Set() var viewModel: SidebarViewModel! weak var delegate: SidebarViewControllerDelegate? - - let settingBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color - barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) - return barButtonItem - }() static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) configuration.backgroundColor = .clear - if sectionIndex == SidebarViewModel.Section.tab.rawValue { - // with indentation - configuration.headerMode = .none - } else { - // remove indentation - configuration.headerMode = .firstItemInSection + configuration.showsSeparators = false + let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) + switch sectionIndex { + case 0: + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(100)), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + section.boundarySupplementaryItems = [header] + default: + break } + return section + } + return layout + } + + let collectionView: UICollectionView = { + let layout = SidebarViewController.createLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.alwaysBounceVertical = false + collectionView.backgroundColor = .clear + return collectionView + }() + + static func createSecondaryLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) + configuration.backgroundColor = .clear configuration.showsSeparators = false let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) return section @@ -50,12 +70,15 @@ final class SidebarViewController: UIViewController, NeedsDependency { return layout } - let collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SidebarViewController.createLayout()) + let secondaryCollectionView: UICollectionView = { + let layout = SidebarViewController.createSecondaryLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = false + collectionView.alwaysBounceVertical = false collectionView.backgroundColor = .clear return collectionView }() - + var secondaryCollectionViewHeightLayoutConstraint: NSLayoutConstraint! } extension SidebarViewController { @@ -63,23 +86,7 @@ extension SidebarViewController { override func viewDidLoad() { super.viewDidLoad() - viewModel.context.authenticationService.activeMastodonAuthenticationBox - .receive(on: DispatchQueue.main) - .sink { [weak self] activeMastodonAuthenticationBox in - guard let self = self else { return } - let domain = activeMastodonAuthenticationBox?.domain - self.navigationItem.backBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.image = UIImage(systemName: "sidebar.leading") - return barButtonItem - }() - self.navigationItem.title = domain - } - .store(in: &disposeBag) - navigationItem.rightBarButtonItem = settingBarButtonItem - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(SidebarViewController.settingBarButtonItemPressed(_:)) - navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.setNavigationBarHidden(true, animated: false) setupBackground(theme: ThemeService.shared.currentTheme.value) ThemeService.shared.currentTheme @@ -99,65 +106,102 @@ extension SidebarViewController { collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + secondaryCollectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(secondaryCollectionView) + secondaryCollectionViewHeightLayoutConstraint = secondaryCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1) + NSLayoutConstraint.activate([ + secondaryCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + secondaryCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: secondaryCollectionView.bottomAnchor), + secondaryCollectionViewHeightLayoutConstraint, + ]) + collectionView.delegate = self - viewModel.setupDiffableDataSource(collectionView: collectionView) + secondaryCollectionView.delegate = self + viewModel.setupDiffableDataSource( + collectionView: collectionView, + secondaryCollectionView: secondaryCollectionView + ) + + secondaryCollectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] secondaryCollectionView, _ in + guard let self = self else { return } + let height = secondaryCollectionView.contentSize.height + self.secondaryCollectionViewHeightLayoutConstraint.constant = height + self.collectionView.contentInset.bottom = height + } + .store(in: &observations) + + let sidebarLongPressGestureRecognizer = UILongPressGestureRecognizer() + sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) + collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) } private func setupBackground(theme: Theme) { let color: UIColor = theme.sidebarBackgroundColor - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithOpaqueBackground() - barAppearance.backgroundColor = color - barAppearance.shadowColor = .clear - barAppearance.shadowImage = UIImage() // remove separator line - navigationItem.standardAppearance = barAppearance - navigationItem.compactAppearance = barAppearance - navigationItem.scrollEdgeAppearance = barAppearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = barAppearance - } else { - // Fallback on earlier versions - } - view.backgroundColor = color - collectionView.backgroundColor = color + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate { context in + self.collectionView.collectionViewLayout.invalidateLayout() +// // do nothing + } completion: { [weak self] context in +// guard let self = self else { return } + } + } } extension SidebarViewController { - @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) - coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) + @objc private func sidebarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { + guard sender.state == .began else { return } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + assert(sender.view === collectionView) + + let position = sender.location(in: collectionView) + guard let indexPath = collectionView.indexPathForItem(at: position) else { return } + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard let cell = collectionView.cellForItem(at: indexPath) else { return } + delegate?.sidebarViewController(self, didLongPressItem: item, sourceView: cell) } + } // MARK: - UICollectionViewDelegate extension SidebarViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let diffableDataSource = viewModel.diffableDataSource else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - switch item { - case .tab(let tab): - delegate?.sidebarViewController(self, didSelectTab: tab) - case .searchHistory(let viewModel): - delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel) - case .header: - break - case .account(let viewModel): - assert(Thread.isMainThread) - let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication - context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - self.coordinator.setup() - } - .store(in: &disposeBag) - case .addAccount: - coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + switch collectionView { + case self.collectionView: + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .tab(let tab): + delegate?.sidebarViewController(self, didSelectTab: tab) + case .setting: + guard let setting = context.settingService.currentSetting.value else { return } + let settingsViewModel = SettingsViewModel(context: context, setting: setting) + coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) + case .compose: + assertionFailure() + } + case secondaryCollectionView: + guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .compose: + let composeViewModel = ComposeViewModel(context: context, composeKind: .post) + coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + default: + assertionFailure() + } + default: + assertionFailure() } } } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index d7ec5b717..83abf4e6b 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -22,6 +22,8 @@ final class SidebarViewModel { // output var diffableDataSource: UICollectionViewDiffableDataSource? + var secondaryDiffableDataSource: UICollectionViewDiffableDataSource? + let activeMastodonAuthenticationObjectID = CurrentValueSubject(nil) init(context: AppContext) { @@ -47,38 +49,22 @@ final class SidebarViewModel { extension SidebarViewModel { enum Section: Int, Hashable, CaseIterable { - case tab - case account + case main + case secondary } enum Item: Hashable { case tab(MainTabBarController.Tab) - case searchHistory(SearchHistoryViewModel) - case header(HeaderViewModel) - case account(AccountViewModel) - case addAccount + case setting + case compose } - struct SearchHistoryViewModel: Hashable { - let searchHistoryObjectID: NSManagedObjectID - } - - struct HeaderViewModel: Hashable { - let title: String - } - - struct AccountViewModel: Hashable { - let authenticationObjectID: NSManagedObjectID - } - - struct AddAccountViewModel: Hashable { - let id = UUID() - } } extension SidebarViewModel { func setupDiffableDataSource( - collectionView: UICollectionView + collectionView: UICollectionView, + secondaryCollectionView: UICollectionView ) { let tabCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in guard let self = self else { return } @@ -92,25 +78,14 @@ extension SidebarViewModel { return nil } }() - let headline: MetaContent = { - switch item { - case .me: - return PlaintextMetaContent(string: item.title) - // TODO: - // return PlaintextMetaContent(string: "Myself") - default: - return PlaintextMetaContent(string: item.title) - } - }() - let needsOutlineDisclosure = item == .search cell.item = SidebarListContentView.Item( + title: item.title, image: item.sidebarImage, - imageURL: imageURL, - headline: headline, - subheadline: nil, - needsOutlineDisclosure: needsOutlineDisclosure + imageURL: imageURL ) cell.setNeedsUpdateConfiguration() + cell.isAccessibilityElement = true + cell.accessibilityLabel = item.title switch item { case .notification: @@ -130,214 +105,102 @@ extension SidebarViewModel { cell._contentView?.imageView.image = image } .store(in: &cell.disposeBag) + case .me: + guard let authentication = self.context.authenticationService.activeMastodonAuthentication.value else { break } + let currentUserDisplayName = authentication.user.displayNameWithFallback ?? "no user" + cell.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName) default: break } } - let searchHistoryCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in + let cellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in guard let self = self else { return } - let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext - - guard let searchHistory = try? managedObjectContext.existingObject(with: item.searchHistoryObjectID) as? SearchHistory else { return } - - if let account = searchHistory.account { - let headline: MetaContent = { - do { - let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta) - return try MastodonMetaContent.convert(document: content) - } catch { - return PlaintextMetaContent(string: account.displayNameWithFallback) - } - }() - cell.item = SidebarListContentView.Item( - image: .placeholder(color: .systemFill), - imageURL: account.avatarImageURL(), - headline: headline, - subheadline: PlaintextMetaContent(string: "@" + account.acctWithDomain), - needsOutlineDisclosure: false - ) - } else if let hashtag = searchHistory.hashtag { - let image = UIImage(systemName: "number.square.fill")!.withRenderingMode(.alwaysTemplate) - let headline = PlaintextMetaContent(string: "#" + hashtag.name) - cell.item = SidebarListContentView.Item( - image: image, - imageURL: nil, - headline: headline, - subheadline: nil, - needsOutlineDisclosure: false - ) - } else { - assertionFailure() - } - + cell.item = item cell.setNeedsUpdateConfiguration() + cell.isAccessibilityElement = true + cell.accessibilityLabel = item.title } - let headerRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = UIListContentConfiguration.sidebarHeader() - content.text = item.title - cell.contentConfiguration = content - cell.accessories = [.outlineDisclosure()] - } - - let accountRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, item) in - guard let self = self else { return } - - // accounts maybe already sign-out - // check isDeleted before using - guard let authentication = try? AppContext.shared.managedObjectContext.existingObject(with: item.authenticationObjectID) as? MastodonAuthentication, - !authentication.isDeleted else { - return - } - let user = authentication.user - let imageURL = user.avatarImageURL() - let headline: MetaContent = { - do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta) - return try MastodonMetaContent.convert(document: content) - } catch { - return PlaintextMetaContent(string: user.displayNameWithFallback) - } - }() - cell.item = SidebarListContentView.Item( - image: .placeholder(color: .systemFill), - imageURL: imageURL, - headline: headline, - subheadline: PlaintextMetaContent(string: "@" + user.acctWithDomain), - needsOutlineDisclosure: false - ) - cell.setNeedsUpdateConfiguration() - - // FIXME: use notification, not timer - let accessToken = authentication.userAccessToken - AppContext.shared.timestampUpdatePublisher - .map { _ in UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) } - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak cell] count in - guard let cell = cell else { return } - cell._contentView?.badgeButton.setBadge(number: count) - } - .store(in: &cell.disposeBag) - - let authenticationObjectID = item.authenticationObjectID - self.activeMastodonAuthenticationObjectID - .receive(on: DispatchQueue.main) - .sink { [weak cell] objectID in - guard let cell = cell else { return } - cell._contentView?.checkmarkImageView.isHidden = authenticationObjectID != objectID - } - .store(in: &cell.disposeBag) - } - - let addAccountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = UIListContentConfiguration.sidebarCell() - content.text = L10n.Scene.AccountList.addAccount - content.image = UIImage(systemName: "plus.square.fill")! - - cell.contentConfiguration = content - cell.accessories = [] + // header + let headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in + // do nothing } let _diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { case .tab(let tab): return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab) - case .searchHistory(let viewModel): - return collectionView.dequeueConfiguredReusableCell(using: searchHistoryCellRegistration, for: indexPath, item: viewModel) - case .header(let viewModel): - return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel) - case .account(let viewModel): - return collectionView.dequeueConfiguredReusableCell(using: accountRegistration, for: indexPath, item: viewModel) - case .addAccount: - return collectionView.dequeueConfiguredReusableCell(using: addAccountRegistration, for: indexPath, item: AddAccountViewModel()) + case .setting: + let item = SidebarListContentView.Item( + title: L10n.Common.Controls.Actions.settings, + image: UIImage(systemName: "gear")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + case .compose: + let item = SidebarListContentView.Item( + title: "Compose", // TODO: update i18n + image: UIImage(systemName: "square.and.pencil")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } + } + _diffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in + switch elementKind { + case UICollectionView.elementKindSectionHeader: + return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath) + default: + assertionFailure() + return UICollectionReusableView() } } diffableDataSource = _diffableDataSource var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(Section.allCases) - _diffableDataSource.apply(snapshot) + snapshot.appendSections([.main]) - for section in Section.allCases { - switch section { - case .tab: - var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() - let items: [Item] = [ - .tab(.home), - .tab(.search), - .tab(.notification), - .tab(.me), - ] - sectionSnapshot.append(items, to: nil) - _diffableDataSource.apply(sectionSnapshot, to: section) - case .account: - var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() - let headerItem = Item.header(HeaderViewModel(title: "Accounts")) - sectionSnapshot.append([headerItem], to: nil) - sectionSnapshot.append([], to: headerItem) - sectionSnapshot.append([.addAccount], to: headerItem) - sectionSnapshot.expand([headerItem]) - _diffableDataSource.apply(sectionSnapshot, to: section) + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let items: [Item] = [ + .tab(.home), + .tab(.search), + .tab(.notification), + .tab(.me), + .setting, + ] + sectionSnapshot.append(items, to: nil) + _diffableDataSource.apply(sectionSnapshot, to: .main) + + + // secondary + let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource(collectionView: secondaryCollectionView) { collectionView, indexPath, item in + guard case .compose = item else { + assertionFailure() + return UICollectionViewCell() } + + let item = SidebarListContentView.Item( + title: "Compose", // FIXME: + image: UIImage(systemName: "square.and.pencil")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) } +// _secondaryDiffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in +// return nil +// } + secondaryDiffableDataSource = _secondaryDiffableDataSource - // update .search tab - searchHistoryFetchedResultController.objectIDs - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] objectIDs in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } + var secondarySnapshot = NSDiffableDataSourceSnapshot() + secondarySnapshot.appendSections([.secondary]) - // update .search tab - var sectionSnapshot = diffableDataSource.snapshot(for: .tab) - - // remove children - let searchHistorySnapshot = sectionSnapshot.snapshot(of: .tab(.search)) - sectionSnapshot.delete(searchHistorySnapshot.items) - - // append children - let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext - let items: [Item] = objectIDs.compactMap { objectID -> Item? in - guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { return nil } - guard searchHistory.account != nil || searchHistory.hashtag != nil else { return nil } - let viewModel = SearchHistoryViewModel(searchHistoryObjectID: objectID) - return Item.searchHistory(viewModel) - } - sectionSnapshot.append(Array(items.prefix(5)), to: .tab(.search)) - sectionSnapshot.expand([.tab(.search)]) - - // apply snapshot - diffableDataSource.apply(sectionSnapshot, to: .tab, animatingDifferences: false) - } - .store(in: &disposeBag) - - // update .me tab and .account section - context.authenticationService.mastodonAuthentications - .receive(on: DispatchQueue.main) - .sink { [weak self] authentications in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - // tab - var snapshot = diffableDataSource.snapshot() - snapshot.reloadItems([.tab(.me)]) - diffableDataSource.apply(snapshot) - - // account - var accountSectionSnapshot = NSDiffableDataSourceSectionSnapshot() - let headerItem = Item.header(HeaderViewModel(title: "Accounts")) - accountSectionSnapshot.append([headerItem], to: nil) - let accountItems = authentications.map { authentication in - Item.account(AccountViewModel(authenticationObjectID: authentication.objectID)) - } - accountSectionSnapshot.append(accountItems, to: headerItem) - accountSectionSnapshot.append([.addAccount], to: headerItem) - accountSectionSnapshot.expand([headerItem]) - diffableDataSource.apply(accountSectionSnapshot, to: .account) - } - .store(in: &disposeBag) + var secondarySectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let secondarySectionItems: [Item] = [ + .compose, + ] + secondarySectionSnapshot.append(secondarySectionItems, to: nil) + _secondaryDiffableDataSource.apply(secondarySectionSnapshot, to: .secondary) } } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift index 1bb76f59e..998d3f9e2 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift @@ -51,30 +51,9 @@ extension SidebarListCollectionViewCell { newConfiguration.item = item contentConfiguration = newConfiguration + // remove background var newBackgroundConfiguration = UIBackgroundConfiguration.listSidebarCell().updated(for: state) - // Customize the background color to use the tint color when the cell is highlighted or selected. - if state.isSelected || state.isHighlighted { - newBackgroundConfiguration.backgroundColor = Asset.Colors.brandBlue.color - } - if state.isHighlighted { - newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) } - } - - + newBackgroundConfiguration.backgroundColor = .clear backgroundConfiguration = newBackgroundConfiguration - - let needsOutlineDisclosure = item?.needsOutlineDisclosure ?? false - if !needsOutlineDisclosure { - accessories = [] - } else { - let tintColor: UIColor = state.isHighlighted || state.isSelected ? .white : Asset.Colors.brandBlue.color - accessories = [ - UICellAccessory.outlineDisclosure( - displayed: .always, - options: UICellAccessory.OutlineDisclosureOptions(tintColor: tintColor), - actionHandler: nil - ) - ] - } } } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 62b188325..d85d3a8be 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -15,15 +15,11 @@ final class SidebarListContentView: UIView, UIContentView { let logger = Logger(subsystem: "SidebarListContentView", category: "UI") let imageView = UIImageView() - let animationImageView = FLAnimatedImageView() // for animation image - let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false)) - let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false)) - let badgeButton = BadgeButton() - let checkmarkImageView: UIImageView = { - let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold)) - let imageView = UIImageView(image: image) - imageView.tintColor = .label - return imageView + let avatarButton: CircleAvatarButton = { + let button = CircleAvatarButton() + button.borderWidth = 2 + button.borderColor = UIColor.label.cgColor + return button }() private var currentConfiguration: ContentConfiguration! @@ -53,93 +49,32 @@ final class SidebarListContentView: UIView, UIContentView { extension SidebarListContentView { private func _init() { - let imageViewContainer = UIView() - imageViewContainer.translatesAutoresizingMaskIntoConstraints = false - addSubview(imageViewContainer) - NSLayoutConstraint.activate([ - imageViewContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - imageViewContainer.centerYAnchor.constraint(equalTo: centerYAnchor), - ]) - imageViewContainer.setContentHuggingPriority(.defaultLow, for: .horizontal) - imageViewContainer.setContentHuggingPriority(.defaultLow, for: .vertical) - - animationImageView.translatesAutoresizingMaskIntoConstraints = false - imageViewContainer.addSubview(animationImageView) - NSLayoutConstraint.activate([ - animationImageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), - animationImageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), - animationImageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), - animationImageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) - animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageViewContainer.addSubview(imageView) + addSubview(imageView) NSLayoutConstraint.activate([ - imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), - imageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), - imageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 16), + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16), + imageView.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1), + imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) - imageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) - imageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) - - let textContainer = UIStackView() - textContainer.axis = .vertical - textContainer.translatesAutoresizingMaskIntoConstraints = false - addSubview(textContainer) + + avatarButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(avatarButton) NSLayoutConstraint.activate([ - textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10), - textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10), - // textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12), + avatarButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2), + avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2), ]) - - textContainer.addArrangedSubview(headlineLabel) - textContainer.addArrangedSubview(subheadlineLabel) - headlineLabel.setContentHuggingPriority(.required - 9, for: .vertical) - headlineLabel.setContentCompressionResistancePriority(.required - 9, for: .vertical) - subheadlineLabel.setContentHuggingPriority(.required - 10, for: .vertical) - subheadlineLabel.setContentCompressionResistancePriority(.required - 10, for: .vertical) - - badgeButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(badgeButton) - NSLayoutConstraint.activate([ - badgeButton.leadingAnchor.constraint(equalTo: textContainer.trailingAnchor, constant: 4), - badgeButton.centerYAnchor.constraint(equalTo: centerYAnchor), - badgeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 16).priority(.required - 1), - badgeButton.widthAnchor.constraint(equalTo: badgeButton.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - badgeButton.setContentHuggingPriority(.required - 10, for: .horizontal) - badgeButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - - NSLayoutConstraint.activate([ - imageViewContainer.heightAnchor.constraint(equalTo: headlineLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - imageViewContainer.widthAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - - checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(checkmarkImageView) - NSLayoutConstraint.activate([ - checkmarkImageView.centerYAnchor.constraint(equalTo: centerYAnchor), - checkmarkImageView.leadingAnchor.constraint(equalTo: badgeButton.trailingAnchor, constant: 16), - checkmarkImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - ]) - checkmarkImageView.setContentHuggingPriority(.required - 9, for: .horizontal) - checkmarkImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - - animationImageView.isUserInteractionEnabled = false - headlineLabel.isUserInteractionEnabled = false - subheadlineLabel.isUserInteractionEnabled = false - + avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) + avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) + imageView.contentMode = .scaleAspectFit - animationImageView.contentMode = .scaleAspectFit - imageView.tintColor = Asset.Colors.brandBlue.color - animationImageView.tintColor = Asset.Colors.brandBlue.color + avatarButton.contentMode = .scaleAspectFit - badgeButton.setBadge(number: 0) - checkmarkImageView.isHidden = true + imageView.isUserInteractionEnabled = false + avatarButton.isUserInteractionEnabled = false } private func apply(configuration: ContentConfiguration) { @@ -151,32 +86,19 @@ extension SidebarListContentView { guard let item = configuration.item else { return } // configure state - imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color - animationImageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color - headlineLabel.setup(style: .sidebarHeadline(isSelected: item.isSelected)) - subheadlineLabel.setup(style: .sidebarSubheadline(isSelected: item.isSelected)) + let tintColor = item.isHighlighted ? ThemeService.tintColor.withAlphaComponent(0.5) : ThemeService.tintColor + imageView.tintColor = tintColor + avatarButton.tintColor = tintColor // configure model imageView.isHidden = item.imageURL != nil - animationImageView.isHidden = item.imageURL == nil + avatarButton.isHidden = item.imageURL == nil imageView.image = item.image.withRenderingMode(.alwaysTemplate) - animationImageView.setImage( + avatarButton.avatarImageView.setImage( url: item.imageURL, - placeholder: animationImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink + placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink scaleToSize: nil ) - animationImageView.layer.masksToBounds = true - animationImageView.layer.cornerCurve = .continuous - animationImageView.layer.cornerRadius = 4 - - headlineLabel.configure(content: item.headline) - - if let subheadline = item.subheadline { - subheadlineLabel.configure(content: subheadline) - subheadlineLabel.isHidden = false - } else { - subheadlineLabel.isHidden = true - } } } @@ -184,29 +106,27 @@ extension SidebarListContentView { struct Item: Hashable { // state var isSelected: Bool = false + var isHighlighted: Bool = false // model + let title: String let image: UIImage let imageURL: URL? - let headline: MetaContent - let subheadline: MetaContent? - - let needsOutlineDisclosure: Bool - + static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool { return lhs.isSelected == rhs.isSelected + && lhs.isHighlighted == rhs.isHighlighted + && lhs.title == rhs.title && lhs.image == rhs.image && lhs.imageURL == rhs.imageURL - && lhs.headline.string == rhs.headline.string - && lhs.subheadline?.string == rhs.subheadline?.string } func hash(into hasher: inout Hasher) { hasher.combine(isSelected) + hasher.combine(isHighlighted) + hasher.combine(title) hasher.combine(image) imageURL.flatMap { hasher.combine($0) } - hasher.combine(headline.string) - subheadline.flatMap { hasher.combine($0.string) } } } @@ -226,9 +146,11 @@ extension SidebarListContentView { if let state = state as? UICellConfigurationState { updatedConfiguration.item?.isSelected = state.isHighlighted || state.isSelected + updatedConfiguration.item?.isHighlighted = state.isHighlighted } else { assertionFailure() updatedConfiguration.item?.isSelected = false + updatedConfiguration.item?.isHighlighted = false } return updatedConfiguration diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift new file mode 100644 index 000000000..2056c5dcd --- /dev/null +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift @@ -0,0 +1,42 @@ +// +// SidebarListHeaderView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-28. +// + +import UIKit + +final class SidebarListHeaderView: UICollectionReusableView { + + let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = Asset.Scene.Sidebar.logo.image + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension SidebarListHeaderView { + private func _init() { + imageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(imageView) + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16), + imageView.widthAnchor.constraint(equalToConstant: 44).priority(.required - 1), + imageView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1), + ]) + } +} diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift index 365c1ee72..2b0c4736d 100644 --- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendAccountsCollectionViewCell.swift @@ -167,6 +167,8 @@ extension SearchRecommendAccountsCollectionViewCell { containerStackView.addArrangedSubview(followButton) followButton.addTarget(self, action: #selector(SearchRecommendAccountsCollectionViewCell.followButtonDidPressed(_:)), for: .touchUpInside) + + displayNameLabel.isUserInteractionEnabled = false } } diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift index e555e1fac..3734bc8a4 100644 --- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift +++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -30,13 +30,7 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { return label }() - let flameIconView: UIImageView = { - let imageView = UIImageView() - let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate) - imageView.image = image - imageView.tintColor = .white - return imageView - }() + let lineChartView = LineChartView() override func prepareForReuse() { super.prepareForReuse() @@ -98,41 +92,52 @@ extension SearchRecommendTagsCollectionViewCell { containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) ]) - - - let horizontalStackView = UIStackView() - horizontalStackView.axis = .horizontal - horizontalStackView.translatesAutoresizingMaskIntoConstraints = false - horizontalStackView.distribution = .fill - hashtagTitleLabel.translatesAutoresizingMaskIntoConstraints = false - hashtagTitleLabel.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) - horizontalStackView.addArrangedSubview(hashtagTitleLabel) - horizontalStackView.setContentHuggingPriority(.required - 1, for: .vertical) - - flameIconView.translatesAutoresizingMaskIntoConstraints = false - horizontalStackView.addArrangedSubview(flameIconView) - flameIconView.setContentHuggingPriority(.required - 1, for: .horizontal) - - containerStackView.addArrangedSubview(horizontalStackView) - peopleLabel.translatesAutoresizingMaskIntoConstraints = false - peopleLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) + containerStackView.addArrangedSubview(hashtagTitleLabel) containerStackView.addArrangedSubview(peopleLabel) - containerStackView.setCustomSpacing(SearchViewController.hashtagPeopleTalkingLabelTop, after: horizontalStackView) + + let lineChartContainer = UIView() + lineChartContainer.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(lineChartContainer) + NSLayoutConstraint.activate([ + lineChartContainer.topAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 12), + lineChartContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: lineChartContainer.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: lineChartContainer.bottomAnchor, constant: 12), + ]) + lineChartContainer.layer.masksToBounds = true + + lineChartView.translatesAutoresizingMaskIntoConstraints = false + lineChartContainer.addSubview(lineChartView) + NSLayoutConstraint.activate([ + lineChartView.topAnchor.constraint(equalTo: lineChartContainer.topAnchor, constant: 4), + lineChartView.leadingAnchor.constraint(equalTo: lineChartContainer.leadingAnchor), + lineChartView.trailingAnchor.constraint(equalTo: lineChartContainer.trailingAnchor), + lineChartContainer.bottomAnchor.constraint(equalTo: lineChartView.bottomAnchor, constant: 4), + ]) + } func config(with tag: Mastodon.Entity.Tag) { hashtagTitleLabel.text = "# " + tag.name - guard let historys = tag.history else { + guard let history = tag.history else { peopleLabel.text = "" return } - let recentHistory = historys.prefix(2) + let recentHistory = history.prefix(2) let peopleAreTalking = recentHistory.compactMap({ Int($0.accounts) }).reduce(0, +) let string = L10n.Scene.Search.Recommend.HashTag.peopleTalking(String(peopleAreTalking)) peopleLabel.text = string - + + lineChartView.data = history + .sorted(by: { $0.day < $1.day }) // latest last + .map { entry in + guard let point = Int(entry.accounts) else { + return .zero + } + return CGFloat(point) + } } } diff --git a/Mastodon/Scene/Search/Search/View/LineChartView.swift b/Mastodon/Scene/Search/Search/View/LineChartView.swift new file mode 100644 index 000000000..a64aa270d --- /dev/null +++ b/Mastodon/Scene/Search/Search/View/LineChartView.swift @@ -0,0 +1,120 @@ +// +// LineChartView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-18. +// + +import UIKit +import Accelerate +import simd + +final class LineChartView: UIView { + + var data: [CGFloat] = [] { + didSet { + setNeedsLayout() + } + } + + let lineShapeLayer = CAShapeLayer() + let gradientLayer = CAGradientLayer() +// let dotShapeLayer = CAShapeLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension LineChartView { + private func _init() { + lineShapeLayer.frame = bounds + gradientLayer.frame = bounds +// dotShapeLayer.frame = bounds + layer.addSublayer(lineShapeLayer) + layer.addSublayer(gradientLayer) +// layer.addSublayer(dotShapeLayer) + + gradientLayer.colors = [ + UIColor.white.withAlphaComponent(0.5).cgColor, + UIColor.white.withAlphaComponent(0).cgColor, + ] + gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) + gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) + } + + override func layoutSubviews() { + super.layoutSubviews() + + lineShapeLayer.frame = bounds + gradientLayer.frame = bounds +// dotShapeLayer.frame = bounds + + guard data.count > 1 else { + lineShapeLayer.path = nil +// dotShapeLayer.path = nil + gradientLayer.isHidden = true + return + } + gradientLayer.isHidden = false + + // Draw smooth chart + guard let maxDataPoint = data.max() else { + return + } + func calculateY(for point: CGFloat, in frame: CGRect) -> CGFloat { + guard maxDataPoint > 0 else { return .zero } + return (1 - point / maxDataPoint) * frame.height + } + + let segmentCount = data.count - 1 + let segmentWidth = bounds.width / CGFloat(segmentCount) + + let points: [CGPoint] = { + var points: [CGPoint] = [] + var x: CGFloat = 0 + for value in data { + let point = CGPoint(x: x, y: calculateY(for: value, in: bounds)) + points.append(point) + x += segmentWidth + } + return points + }() + + guard let linePath = CurveAlgorithm.shared.createCurvedPath(points) else { return } + let dotPath = UIBezierPath() + + if let last = points.last { + dotPath.addArc(withCenter: last, radius: 3, startAngle: 0, endAngle: 2 * .pi, clockwise: true) + } + + lineShapeLayer.lineWidth = 3 + lineShapeLayer.strokeColor = UIColor.white.cgColor + lineShapeLayer.fillColor = UIColor.clear.cgColor + lineShapeLayer.lineJoin = .round + lineShapeLayer.lineCap = .round + lineShapeLayer.path = linePath.cgPath + + let maskPath = UIBezierPath(cgPath: linePath.cgPath) + maskPath.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY)) + maskPath.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY)) + maskPath.close() + let maskLayer = CAShapeLayer() + maskLayer.path = maskPath.cgPath + maskLayer.fillColor = UIColor.red.cgColor + maskLayer.strokeColor = UIColor.clear.cgColor + maskLayer.lineWidth = 0.0 + gradientLayer.mask = maskLayer + +// dotShapeLayer.lineWidth = 3 +// dotShapeLayer.fillColor = Asset.Colors.brandBlue.color.cgColor +// dotShapeLayer.path = dotPath.cgPath + } +} diff --git a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift index a872fca43..0c919e7d5 100644 --- a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift @@ -16,7 +16,7 @@ import MastodonMeta final class SearchResultTableViewCell: UITableViewCell { - let _imageView: AvatarImageView = { + let avatarImageView: AvatarImageView = { let imageView = AvatarImageView() imageView.tintColor = Asset.Colors.Label.primary.color imageView.layer.cornerRadius = 4 @@ -24,6 +24,13 @@ final class SearchResultTableViewCell: UITableViewCell { return imageView }() + let hashtagImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) + imageView.tintColor = Asset.Colors.Label.primary.color + return imageView + }() + let _titleLabel = MetaLabel(style: .statusName) let _subTitleLabel: UILabel = { @@ -43,7 +50,8 @@ final class SearchResultTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - _imageView.af.cancelImageRequest() + avatarImageView.af.cancelImageRequest() + setDisplayAvatarImage() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -74,11 +82,20 @@ extension SearchResultTableViewCell { containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) - _imageView.translatesAutoresizingMaskIntoConstraints = false - containerStackView.addArrangedSubview(_imageView) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(avatarImageView) NSLayoutConstraint.activate([ - _imageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), - _imageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), + avatarImageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), + avatarImageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), + ]) + + hashtagImageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addSubview(hashtagImageView) + NSLayoutConstraint.activate([ + hashtagImageView.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + hashtagImageView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), + hashtagImageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), + hashtagImageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), ]) let textStackView = UIStackView() @@ -107,7 +124,9 @@ extension SearchResultTableViewCell { _titleLabel.isUserInteractionEnabled = false _subTitleLabel.isUserInteractionEnabled = false - _imageView.isUserInteractionEnabled = false + avatarImageView.isUserInteractionEnabled = false + + setDisplayAvatarImage() } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -182,8 +201,7 @@ extension SearchResultTableViewCell { func config(with tag: Mastodon.Entity.Tag) { configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil)) - let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) - _imageView.image = image + setDisplayHashtagImage() let metaContent = PlaintextMetaContent(string: "#" + tag.name) _titleLabel.configure(content: metaContent) guard let histories = tag.history else { @@ -198,8 +216,7 @@ extension SearchResultTableViewCell { func config(with tag: Tag) { configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil)) - let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) - _imageView.image = image + setDisplayHashtagImage() let metaContent = PlaintextMetaContent(string: "#" + tag.name) _titleLabel.configure(content: metaContent) guard let histories = tag.histories?.sorted(by: { @@ -215,11 +232,23 @@ extension SearchResultTableViewCell { } } +extension SearchResultTableViewCell { + func setDisplayAvatarImage() { + avatarImageView.alpha = 1 + hashtagImageView.alpha = 0 + } + + func setDisplayHashtagImage() { + avatarImageView.alpha = 0 + hashtagImageView.alpha = 1 + } +} + // MARK: - AvatarStackedImageView extension SearchResultTableViewCell: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { CGSize(width: 42, height: 42) } static var configurableAvatarImageCornerRadius: CGFloat { 4 } - var configurableAvatarImageView: FLAnimatedImageView? { _imageView } + var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView } } #if canImport(SwiftUI) && DEBUG @@ -231,7 +260,7 @@ struct SearchResultTableViewCell_Previews: PreviewProvider { UIViewPreview { let cell = SearchResultTableViewCell() cell.backgroundColor = .white - cell._imageView.image = UIImage(systemName: "number.circle.fill") + cell.setDisplayHashtagImage() cell._titleLabel.text = "Electronic Frontier Foundation" cell._subTitleLabel.text = "@eff@mastodon.social" return cell diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 3b4e522e6..04c343647 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -439,7 +439,7 @@ extension SettingsViewController { .sink { _ in // do nothing } receiveValue: { _ in - // do nohting + // do nothing } .store(in: &disposeBag) } @@ -451,16 +451,19 @@ extension SettingsViewController: SettingsAppearanceTableViewCellDelegate { guard let dataSource = viewModel.dataSource else { return } guard let indexPath = tableView.indexPath(for: cell) else { return } let item = dataSource.itemIdentifier(for: indexPath) - guard case let .appearance(settingObjectID) = item else { return } + guard case .appearance = item else { return } - context.managedObjectContext.performChanges { - let setting = self.context.managedObjectContext.object(with: settingObjectID) as! Setting - setting.update(appearanceRaw: appearanceMode.rawValue) + switch appearanceMode { + case .automatic: + UserDefaults.shared.customUserInterfaceStyle = .unspecified + case .light: + UserDefaults.shared.customUserInterfaceStyle = .light + case .dark: + UserDefaults.shared.customUserInterfaceStyle = .dark } - .sink { _ in - let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) - feedbackGenerator.impactOccurred() - }.store(in: &disposeBag) + + let feedbackGenerator = UIImpactFeedbackGenerator(style: .light) + feedbackGenerator.impactOccurred() } } diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift index c4eb998e4..a4904136b 100644 --- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift +++ b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift @@ -15,6 +15,7 @@ protocol SettingsAppearanceTableViewCellDelegate: AnyObject { class SettingsAppearanceTableViewCell: UITableViewCell { var disposeBag = Set() + var observations = Set() static let spacing: CGFloat = 18 @@ -59,6 +60,7 @@ class SettingsAppearanceTableViewCell: UITableViewCell { super.prepareForReuse() disposeBag.removeAll() + observations.removeAll() } // MARK: - Methods diff --git a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift index 40272d290..0bc2aeefd 100644 --- a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift +++ b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift @@ -9,12 +9,15 @@ import UIKit final class CircleAvatarButton: AvatarButton { + var borderColor: CGColor = UIColor.systemFill.cgColor + var borderWidth: CGFloat = 1.0 + override func layoutSubviews() { super.layoutSubviews() layer.masksToBounds = true layer.cornerRadius = frame.width * 0.5 - layer.borderColor = UIColor.systemFill.cgColor - layer.borderWidth = 1 + layer.borderColor = borderColor + layer.borderWidth = borderWidth } } diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index 7afabd3a9..957764fa7 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -73,9 +73,10 @@ final class StatusView: UIView { return attributedString } - let headerIconLabel: UILabel = { - let label = UILabel() - label.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage) + let headerIconLabel: MetaLabel = { + let label = MetaLabel(style: .statusHeader) + let attributedString = StatusView.iconAttributedString(image: StatusView.reblogIconImage) + label.configure(attributedString: attributedString) return label }() @@ -125,7 +126,7 @@ final class StatusView: UIView { let revealContentWarningButton: UIButton = { let button = HighlightDimmableButton() button.setImage(UIImage(systemName: "eye", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .medium)), for: .normal) - button.tintColor = Asset.Colors.brandBlue.color + // button.tintColor = Asset.Colors.brandBlue.color return button }() @@ -217,6 +218,7 @@ final class StatusView: UIView { let style = NSMutableParagraphStyle() style.lineSpacing = 5 style.paragraphSpacing = 8 + style.alignment = .natural return style }() metaText.textAttributes = [ diff --git a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift index 4bda525ae..c339654f5 100644 --- a/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift +++ b/Mastodon/Scene/Share/View/Content/ThreadMetaView.swift @@ -23,8 +23,8 @@ final class ThreadMetaView: UIView { let button = UIButton() button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) button.setTitle("0 reblog", for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color.withAlphaComponent(0.5), for: .highlighted) + button.setTitleColor(ThemeService.tintColor, for: .normal) + button.setTitleColor(ThemeService.tintColor.withAlphaComponent(0.5), for: .highlighted) return button }() @@ -32,8 +32,8 @@ final class ThreadMetaView: UIView { let button = UIButton() button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold)) button.setTitle("0 favorite", for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) - button.setTitleColor(Asset.Colors.brandBlue.color.withAlphaComponent(0.5), for: .highlighted) + button.setTitleColor(ThemeService.tintColor, for: .normal) + button.setTitleColor(ThemeService.tintColor.withAlphaComponent(0.5), for: .highlighted) return button }() diff --git a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift index 5e5ac88d7..a819f301c 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/ThreadReplyLoaderTableViewCell.swift @@ -23,7 +23,7 @@ final class ThreadReplyLoaderTableViewCell: UITableViewCell { let loadMoreButton: UIButton = { let button = HighlightDimmableButton() button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) + button.setTitleColor(ThemeService.tintColor, for: .normal) button.setTitle(L10n.Common.Controls.Timeline.Loader.showMoreReplies, for: .normal) return button }() diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineFooterTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineFooterTableViewCell.swift new file mode 100644 index 000000000..43dd2c6fa --- /dev/null +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineFooterTableViewCell.swift @@ -0,0 +1,51 @@ +// +// TimelineFooterTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import UIKit + +final class TimelineFooterTableViewCell: UITableViewCell { + + let messageLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 17) + label.textAlignment = .center + label.textColor = Asset.Colors.Label.secondary.color + label.text = "info" + label.numberOfLines = 0 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension TimelineFooterTableViewCell { + + private func _init() { + selectionStyle = .none + backgroundColor = .clear + + messageLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(messageLabel) + NSLayoutConstraint.activate([ + messageLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + messageLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + messageLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 68).priority(.required - 1), // same height to bottom loader + ]) + } + +} diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift index 8c329d31e..da0b80fb4 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift @@ -24,7 +24,7 @@ class TimelineLoaderTableViewCell: UITableViewCell { let loadMoreButton: UIButton = { let button = HighlightDimmableButton() button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont - button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal) + button.setTitleColor(ThemeService.tintColor, for: .normal) button.setTitle(L10n.Common.Controls.Timeline.Loader.loadMissingPosts, for: .normal) button.setTitle("", for: .disabled) return button @@ -68,7 +68,7 @@ class TimelineLoaderTableViewCell: UITableViewCell { func stopAnimating() { activityIndicatorView.stopAnimating() self.loadMoreButton.isEnabled = true - self.loadMoreLabel.textColor = Asset.Colors.brandBlue.color + self.loadMoreLabel.textColor = ThemeService.tintColor self.loadMoreLabel.text = "" } diff --git a/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift new file mode 100644 index 000000000..29e28415e --- /dev/null +++ b/Mastodon/Scene/Share/View/TableviewCell/UserTableViewCell.swift @@ -0,0 +1,131 @@ +// +// UserTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import CoreData +import CoreDataStack +import MastodonSDK +import UIKit +import MetaTextKit +import MastodonMeta +import FLAnimatedImage + +protocol UserTableViewCellDelegate: AnyObject { } + +final class UserTableViewCell: UITableViewCell { + + weak var delegate: UserTableViewCellDelegate? + + let avatarImageView: AvatarImageView = { + let imageView = AvatarImageView() + imageView.tintColor = Asset.Colors.Label.primary.color + imageView.layer.cornerRadius = 4 + imageView.clipsToBounds = true + return imageView + }() + + let nameLabel = MetaLabel(style: .statusName) + + let usernameLabel: UILabel = { + let label = UILabel() + label.textColor = Asset.Colors.Label.secondary.color + label.font = .preferredFont(forTextStyle: .body) + return label + }() + + let separatorLine = UIView.separatorLine + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension UserTableViewCell { + + private func _init() { + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.distribution = .fill + containerStackView.spacing = 12 + containerStackView.layoutMargins = UIEdgeInsets(top: 12, left: 0, bottom: 12, right: 0) + containerStackView.isLayoutMarginsRelativeArrangement = true + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 42).priority(.required - 1), + avatarImageView.heightAnchor.constraint(equalToConstant: 42).priority(.required - 1), + ]) + + let textStackView = UIStackView() + textStackView.axis = .vertical + textStackView.distribution = .fill + textStackView.translatesAutoresizingMaskIntoConstraints = false + nameLabel.translatesAutoresizingMaskIntoConstraints = false + textStackView.addArrangedSubview(nameLabel) + usernameLabel.translatesAutoresizingMaskIntoConstraints = false + textStackView.addArrangedSubview(usernameLabel) + usernameLabel.setContentHuggingPriority(.defaultLow - 1, for: .vertical) + + containerStackView.addArrangedSubview(textStackView) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)), + ]) + + + nameLabel.isUserInteractionEnabled = false + usernameLabel.isUserInteractionEnabled = false + avatarImageView.isUserInteractionEnabled = false + } + +} + +// MARK: - AvatarStackedImageView +extension UserTableViewCell: AvatarConfigurableView { + static var configurableAvatarImageSize: CGSize { CGSize(width: 42, height: 42) } + static var configurableAvatarImageCornerRadius: CGFloat { 4 } + var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView } +} + +extension UserTableViewCell { + func configure(user: MastodonUser) { + // avatar + configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: user.avatarImageURL())) + // name + let name = user.displayNameWithFallback + do { + let mastodonContent = MastodonContent(content: name, emojis: user.emojiMeta) + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + nameLabel.configure(content: metaContent) + } catch { + let metaContent = PlaintextMetaContent(string: name) + nameLabel.configure(content: metaContent) + } + // username + usernameLabel.text = "@" + user.acct + } +} diff --git a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift index e6e111018..f8f5d3e7e 100644 --- a/Mastodon/Scene/Thread/RemoteThreadViewModel.swift +++ b/Mastodon/Scene/Thread/RemoteThreadViewModel.swift @@ -48,7 +48,6 @@ final class RemoteThreadViewModel: ThreadViewModel { .store(in: &disposeBag) } - // FIXME: multiple account supports init(context: AppContext, notificationID: Mastodon.Entity.Notification.ID) { super.init(context: context, optionalStatus: nil) diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 5bbc383e4..853bee9da 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -135,7 +135,7 @@ extension ThreadViewModel { // save height before cell reuse let oldRootCellHeight = oldRootCell?.frame.height - diffableDataSource.apply(newSnapshot, animatingDifferences: false) { + diffableDataSource.reloadData(snapshot: newSnapshot) { guard let _ = rootItem else { return } diff --git a/Mastodon/Service/APIService/APIService+Follower.swift b/Mastodon/Service/APIService/APIService+Follower.swift new file mode 100644 index 000000000..db29a0a29 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Follower.swift @@ -0,0 +1,65 @@ +// +// APIService+Follower.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack +import CommonOSLog +import MastodonSDK + +extension APIService { + + func followers( + userID: Mastodon.Entity.Account.ID, + maxID: String?, + authorizationBox: MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let domain = authorizationBox.domain + let authorization = authorizationBox.userAuthorization + let requestMastodonUserID = authorizationBox.userID + + return Mastodon.API.Account.followers( + session: session, + domain: domain, + userID: userID, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + let managedObjectContext = self.backgroundManagedObjectContext + return managedObjectContext.performChanges { + let requestMastodonUserRequest = MastodonUser.sortedFetchRequest + requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID) + requestMastodonUserRequest.fetchLimit = 1 + guard let requestMastodonUser = managedObjectContext.safeFetch(requestMastodonUserRequest).first else { return } + + for entity in response.value { + _ = APIService.CoreData.createOrMergeMastodonUser( + into: managedObjectContext, + for: requestMastodonUser, + in: domain, + entity: entity, + userCache: nil, + networkDate: response.networkDate, + log: .api + ) + } + } + .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Account]> in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift new file mode 100644 index 000000000..614d098aa --- /dev/null +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+Instance.swift @@ -0,0 +1,76 @@ +// +// APIService+CoreData+Instance.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-9. +// + +import os.log +import Foundation +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService.CoreData { + + static func createOrMergeInstance( + into managedObjectContext: NSManagedObjectContext, + domain: String, + entity: Mastodon.Entity.Instance, + networkDate: Date, + log: OSLog + ) -> (instance: Instance, isCreated: Bool) { + // fetch old mastodon user + let old: Instance? = { + let request = Instance.sortedFetchRequest + request.predicate = Instance.predicate(domain: domain) + request.fetchLimit = 1 + request.returnsObjectsAsFaults = false + do { + return try managedObjectContext.fetch(request).first + } catch { + assertionFailure(error.localizedDescription) + return nil + } + }() + + if let old = old { + // merge old + APIService.CoreData.merge( + instance: old, + entity: entity, + domain: domain, + networkDate: networkDate + ) + return (old, false) + } else { + let instance = Instance.insert( + into: managedObjectContext, + property: Instance.Property(domain: domain) + ) + let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) } + instance.update(configurationRaw: configurationRaw) + + return (instance, true) + } + } + +} + +extension APIService.CoreData { + + static func merge( + instance: Instance, + entity: Mastodon.Entity.Instance, + domain: String, + networkDate: Date + ) { + guard networkDate > instance.updatedAt else { return } + + let configurationRaw = entity.configuration.flatMap { Instance.encode(configuration: $0) } + instance.update(configurationRaw: configurationRaw) + + instance.didUpdate(at: networkDate) + } + +} diff --git a/Mastodon/Service/AuthenticationService.swift b/Mastodon/Service/AuthenticationService.swift index 0b3c3fa11..9e27caab6 100644 --- a/Mastodon/Service/AuthenticationService.swift +++ b/Mastodon/Service/AuthenticationService.swift @@ -95,8 +95,11 @@ extension AuthenticationService { func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher, Never> { var isActive = false + var _mastodonAuthentication: MastodonAuthentication? - return backgroundManagedObjectContext.performChanges { + return backgroundManagedObjectContext.performChanges { [weak self] in + guard let self = self else { return } + let request = MastodonAuthentication.sortedFetchRequest request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID) request.fetchLimit = 1 @@ -104,9 +107,29 @@ extension AuthenticationService { return } mastodonAuthentication.update(activedAt: Date()) + _mastodonAuthentication = mastodonAuthentication isActive = true + } - .map { result in + .receive(on: DispatchQueue.main) + .map { [weak self] result in + switch result { + case .success: + if let self = self, + let mastodonAuthentication = _mastodonAuthentication + { + // force set to avoid delay + self.activeMastodonAuthentication.value = mastodonAuthentication + self.activeMastodonAuthenticationBox.value = MastodonAuthenticationBox( + domain: mastodonAuthentication.domain, + userID: mastodonAuthentication.userID, + appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.appAccessToken), + userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: mastodonAuthentication.userAccessToken) + ) + } + case .failure: + break + } return result.map { isActive } } .eraseToAnyPublisher() diff --git a/Mastodon/Service/InstanceService.swift b/Mastodon/Service/InstanceService.swift new file mode 100644 index 000000000..4fb6309fd --- /dev/null +++ b/Mastodon/Service/InstanceService.swift @@ -0,0 +1,103 @@ +// +// InstanceService.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-9. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +final class InstanceService { + + var disposeBag = Set() + + let logger = Logger(subsystem: "InstanceService", category: "Logic") + + // input + let backgroundManagedObjectContext: NSManagedObjectContext + weak var apiService: APIService? + weak var authenticationService: AuthenticationService? + + // output + + init( + apiService: APIService, + authenticationService: AuthenticationService + ) { + self.backgroundManagedObjectContext = apiService.backgroundManagedObjectContext + self.apiService = apiService + self.authenticationService = authenticationService + + authenticationService.activeMastodonAuthenticationBox + .receive(on: DispatchQueue.main) + .compactMap { $0?.domain } + .removeDuplicates() // prevent infinity loop + .sink { [weak self] domain in + guard let self = self else { return } + self.updateInstance(domain: domain) + } + .store(in: &disposeBag) + } + +} + +extension InstanceService { + func updateInstance(domain: String) { + guard let apiService = self.apiService else { return } + apiService.instance(domain: domain) + .flatMap { response -> AnyPublisher, Error> in + let managedObjectContext = self.backgroundManagedObjectContext + return managedObjectContext.performChanges { + // get instance + let (instance, _) = APIService.CoreData.createOrMergeInstance( + into: managedObjectContext, + domain: domain, + entity: response.value, + networkDate: response.networkDate, + log: OSLog.api + ) + + // update relationship + let request = MastodonAuthentication.sortedFetchRequest + request.predicate = MastodonAuthentication.predicate(domain: domain) + request.returnsObjectsAsFaults = false + do { + let authentications = try managedObjectContext.fetch(request) + for authentication in authentications { + authentication.update(instance: instance) + } + } catch { + assertionFailure(error.localizedDescription) + } + } + .setFailureType(to: Error.self) + .tryMap { result -> Mastodon.Response.Content in + switch result { + case .success: + return response + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update instance failure: \(error.localizedDescription)") + case .finished: + self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Instance] update instance for domain: \(domain)") + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + // do nothing + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index 463a2def8..6eb3120c7 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -30,7 +30,7 @@ final class NotificationService { /// [Token: NotificationViewModel] let notificationSubscriptionDict: [String: NotificationViewModel] = [:] let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) - let requestRevealNotificationPublisher = PassthroughSubject() + let requestRevealNotificationPublisher = PassthroughSubject() init( apiService: APIService, diff --git a/Mastodon/Service/SettingService.swift b/Mastodon/Service/SettingService.swift index 1c030c519..79ed47abf 100644 --- a/Mastodon/Service/SettingService.swift +++ b/Mastodon/Service/SettingService.swift @@ -54,8 +54,7 @@ final class SettingService { into: managedObjectContext, property: Setting.Property( domain: domain, - userID: userID, - appearanceRaw: SettingsItem.AppearanceMode.automatic.rawValue + userID: userID ) ) } // end for @@ -190,16 +189,16 @@ extension SettingService { static func updatePreference(setting: Setting) { // set appearance - let userInterfaceStyle: UIUserInterfaceStyle = { - switch setting.appearance { - case .automatic: return .unspecified - case .light: return .light - case .dark: return .dark - } - }() - if UserDefaults.shared.customUserInterfaceStyle != userInterfaceStyle { - UserDefaults.shared.customUserInterfaceStyle = userInterfaceStyle - } +// let userInterfaceStyle: UIUserInterfaceStyle = { +// switch setting.appearance { +// case .automatic: return .unspecified +// case .light: return .light +// case .dark: return .dark +// } +// }() +// if UserDefaults.shared.customUserInterfaceStyle != userInterfaceStyle { +// UserDefaults.shared.customUserInterfaceStyle = userInterfaceStyle +// } // set theme let themeName: ThemeName = setting.preferredTrueBlackDarkMode ? .system : .mastodon diff --git a/Mastodon/Service/ThemeService/MastodonTheme.swift b/Mastodon/Service/ThemeService/MastodonTheme.swift index 85b0d42db..1f0fd4e38 100644 --- a/Mastodon/Service/ThemeService/MastodonTheme.swift +++ b/Mastodon/Service/ThemeService/MastodonTheme.swift @@ -26,7 +26,7 @@ struct MastodonTheme: Theme { let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color let tabBarBackgroundColor = Asset.Theme.Mastodon.tabBarBackground.color - let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color + let tabBarItemSelectedIconColor = ThemeService.tintColor let tabBarItemFocusedIconColor = Asset.Theme.Mastodon.tabBarItemInactiveIconColor.color let tabBarItemNormalIconColor = Asset.Theme.Mastodon.tabBarItemInactiveIconColor.color let tabBarItemDisabledIconColor = Asset.Theme.Mastodon.tabBarItemInactiveIconColor.color diff --git a/Mastodon/Service/ThemeService/SystemTheme.swift b/Mastodon/Service/ThemeService/SystemTheme.swift index 2e3b290db..26673d57d 100644 --- a/Mastodon/Service/ThemeService/SystemTheme.swift +++ b/Mastodon/Service/ThemeService/SystemTheme.swift @@ -23,10 +23,10 @@ struct SystemTheme: Theme { let navigationBarBackgroundColor = Asset.Theme.System.navigationBarBackground.color - let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color + let sidebarBackgroundColor = Asset.Theme.System.sidebarBackground.color let tabBarBackgroundColor = Asset.Theme.System.tabBarBackground.color - let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color + let tabBarItemSelectedIconColor = ThemeService.tintColor let tabBarItemFocusedIconColor = Asset.Theme.System.tabBarItemInactiveIconColor.color let tabBarItemNormalIconColor = Asset.Theme.System.tabBarItemInactiveIconColor.color let tabBarItemDisabledIconColor = Asset.Theme.System.tabBarItemInactiveIconColor.color diff --git a/Mastodon/Service/ThemeService/ThemeService+Appearance.swift b/Mastodon/Service/ThemeService/ThemeService+Appearance.swift index 8130942aa..896ed888e 100644 --- a/Mastodon/Service/ThemeService/ThemeService+Appearance.swift +++ b/Mastodon/Service/ThemeService/ThemeService+Appearance.swift @@ -46,8 +46,13 @@ extension ThemeService { tabBarAppearance.compactInlineLayoutAppearance = tabBarItemAppearance tabBarAppearance.backgroundColor = theme.tabBarBackgroundColor - tabBarAppearance.selectionIndicatorTintColor = Asset.Colors.brandBlue.color + tabBarAppearance.selectionIndicatorTintColor = ThemeService.tintColor UITabBar.appearance().standardAppearance = tabBarAppearance + if #available(iOS 15.0, *) { + UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance + } else { + // Fallback on earlier versions + } UITabBar.appearance().barTintColor = theme.tabBarBackgroundColor // set table view cell appearance @@ -56,9 +61,9 @@ extension ThemeService { UITableViewCell.appearance().selectionColor = theme.tableViewCellSelectionBackgroundColor // set search bar appearance - UISearchBar.appearance().tintColor = Asset.Colors.brandBlue.color + UISearchBar.appearance().tintColor = ThemeService.tintColor UISearchBar.appearance().barTintColor = theme.navigationBarBackgroundColor - let cancelButtonAttributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.foregroundColor: Asset.Colors.brandBlue.color] + let cancelButtonAttributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.foregroundColor: ThemeService.tintColor] UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes(cancelButtonAttributes, for: .normal) } } diff --git a/Mastodon/Service/ThemeService/ThemeService.swift b/Mastodon/Service/ThemeService/ThemeService.swift index 35d5b3491..e3bd7c4ab 100644 --- a/Mastodon/Service/ThemeService/ThemeService.swift +++ b/Mastodon/Service/ThemeService/ThemeService.swift @@ -10,6 +10,8 @@ import Combine // ref: https://zamzam.io/protocol-oriented-themes-for-ios-apps/ final class ThemeService { + + static let tintColor: UIColor = .label // MARK: - Singleton public static let shared = ThemeService() diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index d4682ed5e..d7c08d47f 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -31,6 +31,7 @@ class AppContext: ObservableObject { let statusPublishService = StatusPublishService() let notificationService: NotificationService let settingService: SettingService + let instanceService: InstanceService let blockDomainService: BlockDomainService let statusFilterService: StatusFilterService @@ -87,6 +88,11 @@ class AppContext: ObservableObject { notificationService: _notificationService ) + instanceService = InstanceService( + apiService: _apiService, + authenticationService: _authenticationService + ) + blockDomainService = BlockDomainService( backgroundManagedObjectContext: _backgroundManagedObjectContext, authenticationService: _authenticationService diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 192f201d1..87d16241a 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -109,7 +109,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler([.sound]) } - // response to user action for notification + // response to user action for notification (e.g. redirect to post) func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, @@ -125,7 +125,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let notificationID = String(mastodonPushNotification.notificationID) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) appContext.notificationService.handle(mastodonPushNotification: mastodonPushNotification) - appContext.notificationService.requestRevealNotificationPublisher.send(notificationID) + appContext.notificationService.requestRevealNotificationPublisher.send(mastodonPushNotification) completionHandler() } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 6c5752c4c..4809fe5f9 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -37,7 +37,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = window // set tint color - window.tintColor = Asset.Colors.brandBlue.color + window.tintColor = UIColor.label ThemeService.shared.currentTheme .receive(on: RunLoop.main) diff --git a/Mastodon/Vender/CurveAlgorithm.swift b/Mastodon/Vender/CurveAlgorithm.swift new file mode 100644 index 000000000..0ca4c8734 --- /dev/null +++ b/Mastodon/Vender/CurveAlgorithm.swift @@ -0,0 +1,47 @@ +// +// CurveAlgorithm.swift +// +// Ref: https://github.com/nhatminh12369/LineChart/blob/master/LineChart/CurveAlgorithm.swift + +import UIKit + +struct CurvedSegment { + var controlPoint1: CGPoint + var controlPoint2: CGPoint +} + +class CurveAlgorithm { + static let shared = CurveAlgorithm() + + private func controlPointsFrom(points: [CGPoint]) -> [CurvedSegment] { + var result: [CurvedSegment] = [] + + let delta: CGFloat = 0.2 + + // only use horizontal control point + for i in 1.. UIBezierPath? { + let path = UIBezierPath() + path.move(to: dataPoints[0]) + + var curveSegments: [CurvedSegment] = [] + curveSegments = controlPointsFrom(points: dataPoints) + + for i in 1..CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 NSExtension NSExtensionAttributes diff --git a/MastodonIntent/ar.lproj/Intents.strings b/MastodonIntent/ar.lproj/Intents.strings index bf3e77ed2..cde27dc97 100644 --- a/MastodonIntent/ar.lproj/Intents.strings +++ b/MastodonIntent/ar.lproj/Intents.strings @@ -1,22 +1,22 @@ -"16wxgf" = "Post on Mastodon"; +"16wxgf" = "النَشر على ماستودون"; "751xkl" = "محتوى نصي"; "CsR7G2" = "انشر على ماستدون"; -"HZSGTr" = "What content to post?"; +"HZSGTr" = "ما المُحتوى المُراد نشره؟"; -"HdGikU" = "Posting failed"; +"HdGikU" = "فَشَلَ النشر"; "KDNTJ4" = "سبب الإخفاق"; -"RHxKOw" = "Send Post with text content"; +"RHxKOw" = "إرسال مَنشور يَحوي نص"; -"RxSqsb" = "Post"; +"RxSqsb" = "مَنشور"; -"WCIR3D" = "Post ${content} on Mastodon"; +"WCIR3D" = "نَشر ${content} على ماستودون"; -"ZKJSNu" = "Post"; +"ZKJSNu" = "مَنشور"; "ZS1XaK" = "${content}"; @@ -24,13 +24,13 @@ "Zo4jgJ" = "مدى ظهور المنشور"; -"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’."; +"apSxMG-dYQ5NN" = "هُناك عدد ${count} خِيار مُطابق لِـ\"عام\"."; -"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’."; +"apSxMG-ehFLjY" = "هُناك عدد ${count} خِيار مُطابق لِـ\"المُتابِعُون فقط\"."; -"ayoYEb-dYQ5NN" = "${content}, Public"; +"ayoYEb-dYQ5NN" = "${content}، عام"; -"ayoYEb-ehFLjY" = "${content}, Followers Only"; +"ayoYEb-ehFLjY" = "${content}، المُتابِعُون فقط"; "dUyuGg" = "النشر على ماستدون"; @@ -38,13 +38,13 @@ "ehFLjY" = "لمتابعيك فقط"; -"gfePDu" = "Posting failed. ${failureReason}"; +"gfePDu" = "فَشَلَ النشر، ${failureReason}"; -"k7dbKQ" = "Post was sent successfully."; +"k7dbKQ" = "تمَّ إرسال المنشور بِنجاح."; -"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?"; +"oGiqmY-dYQ5NN" = "للتأكيد، هل تَريد \"عام\"؟"; -"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?"; +"oGiqmY-ehFLjY" = "للتأكيد، هل تُريد \"للمُتابِعين فقط\"؟"; "rM6dvp" = "عنوان URL"; diff --git a/MastodonIntent/ku-TR.lproj/Intents.strings b/MastodonIntent/ku-TR.lproj/Intents.strings new file mode 100644 index 000000000..3e1c69fc3 --- /dev/null +++ b/MastodonIntent/ku-TR.lproj/Intents.strings @@ -0,0 +1,51 @@ +"16wxgf" = "Di Mastodon de biweşîne"; + +"751xkl" = "Naveroka nivîsê"; + +"CsR7G2" = "Di Mastodon de biweşîne"; + +"HZSGTr" = "Kîjan naverok bila bê şandin?"; + +"HdGikU" = "Şandin têkçû"; + +"KDNTJ4" = "Sedema têkçûnê"; + +"RHxKOw" = "Bi naveroka nivîsî şandiyan bişîne"; + +"RxSqsb" = "Şandî"; + +"WCIR3D" = "${content} biweşîne di Mastodon de"; + +"ZKJSNu" = "Şandî"; + +"ZS1XaK" = "${content}"; + +"ZbSjzC" = "Xuyanî"; + +"Zo4jgJ" = "Xuyaniya şandiyê"; + +"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Giştî' ne."; + +"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopandin' hene."; + +"ayoYEb-dYQ5NN" = "${content}, Giştî"; + +"ayoYEb-ehFLjY" = "${content}, Tenê şopînêr"; + +"dUyuGg" = "Li ser Mastodon bişînin"; + +"dYQ5NN" = "Gelemperî"; + +"ehFLjY" = "Tenê şopîneran"; + +"gfePDu" = "Weşandin bi ser neket. ${failureReason}"; + +"k7dbKQ" = "Şandî bi serkeftî hate şandin."; + +"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, we 'Giştî' dixwest?"; + +"oGiqmY-ehFLjY" = "Tenê ji bo piştrastkirinê, we 'Tenê Şopdarên' dixwest?"; + +"rM6dvp" = "Girêdan"; + +"ryJLwG" = "Bi serkeftî hat şandin. "; diff --git a/MastodonIntent/ku-TR.lproj/Intents.stringsdict b/MastodonIntent/ku-TR.lproj/Intents.stringsdict new file mode 100644 index 000000000..5a39d5e64 --- /dev/null +++ b/MastodonIntent/ku-TR.lproj/Intents.stringsdict @@ -0,0 +1,54 @@ + + + + + There are ${count} options matching ‘${content}’. - 2 + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${content}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + There are ${count} options matching ‘${visibility}’. + + NSStringLocalizedFormatKey + There are %#@count_option@ matching ‘${visibility}’. + count_option + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + %ld + zero + 0 options + one + 1 option + two + 2 options + few + %ld options + many + %ld options + other + %ld options + + + + diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift index 87c879ea0..7adbcdeff 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+FollowRequest.swift @@ -12,13 +12,15 @@ import Combine extension Mastodon.API.Account { static func acceptFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("follow_requests") .appendingPathComponent(userID) .appendingPathComponent("authorize") } static func rejectFollowRequestEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("follow_requests") + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("follow_requests") .appendingPathComponent(userID) .appendingPathComponent("reject") } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift new file mode 100644 index 000000000..b09a5f07b --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift @@ -0,0 +1,81 @@ +// +// Mastodon+API+Account+Followers.swift +// +// +// Created by Cirno MainasuK on 2021-11-1. +// + +import Foundation +import Combine + +extension Mastodon.API.Account { + + static func followersEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL { + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("accounts") + .appendingPathComponent(userID) + .appendingPathComponent("followers") + } + + /// Followers + /// + /// Accounts which follow the given account, if network is not hidden by the account owner. + /// + /// - Since: 0.0.0 + /// - Version: 3.4.1 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - userID: ID of the account in the database + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `[Account]` nested in the response + public static func followers( + session: URLSession, + domain: String, + userID: Mastodon.Entity.Account.ID, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: followersEndpointURL(domain: domain, userID: userID), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } + + public struct FollowerQuery: Codable, GetQuery { + + public let maxID: String? + public let limit: Int? // default 40 + + enum CodingKeys: String, CodingKey { + case maxID = "max_id" + case limit + } + + public init( + maxID: String?, + limit: Int? + ) { + self.maxID = maxID + self.limit = limit + } + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + guard !items.isEmpty else { return nil } + return items + } + + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift index 226af40f8..d0d16ee4a 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Instance.swift @@ -34,6 +34,9 @@ extension Mastodon.Entity { public let thumbnail: String? public let contactAccount: Account? public let rules: [Rule]? + + // https://github.com/mastodon/mastodon/pull/16485 + public let configuration: Configuration? enum CodingKeys: String, CodingKey { case uri @@ -52,6 +55,8 @@ extension Mastodon.Entity { case thumbnail case contactAccount = "contact_account" case rules + + case configuration } } } @@ -86,3 +91,63 @@ extension Mastodon.Entity.Instance { public let text: String } } + +extension Mastodon.Entity.Instance { + public struct Configuration: Codable { + public let statuses: Statuses? + public let mediaAttachments: MediaAttachments? + public let polls: Polls? + + enum CodingKeys: String, CodingKey { + case statuses + case mediaAttachments = "media_attachments" + case polls + } + } +} + +extension Mastodon.Entity.Instance.Configuration { + public struct Statuses: Codable { + public let maxCharacters: Int + public let maxMediaAttachments: Int + public let charactersReservedPerURL: Int + + enum CodingKeys: String, CodingKey { + case maxCharacters = "max_characters" + case maxMediaAttachments = "max_media_attachments" + case charactersReservedPerURL = "characters_reserved_per_url" + } + } + + public struct MediaAttachments: Codable { + public let supportedMIMETypes: [String] + public let imageSizeLimit: Int + public let imageMatrixLimit: Int + public let videoSizeLimit: Int + public let videoFrameRateLimit: Int + public let videoMatrixLimit: Int + + enum CodingKeys: String, CodingKey { + case supportedMIMETypes = "supported_mime_types" + case imageSizeLimit = "image_size_limit" + case imageMatrixLimit = "image_matrix_limit" + case videoSizeLimit = "video_size_limit" + case videoFrameRateLimit = "video_frame_rate_limit" + case videoMatrixLimit = "video_matrix_limit" + } + } + + public struct Polls: Codable { + public let maxOptions: Int + public let maxCharactersPerOption: Int + public let minExpiration: Int + public let maxExpiration: Int + + enum CodingKeys: String, CodingKey { + case maxOptions = "max_options" + case maxCharactersPerOption = "max_characters_per_option" + case minExpiration = "min_expiration" + case maxExpiration = "max_expiration" + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift index e7f095eb3..740001572 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Tag.swift @@ -22,6 +22,7 @@ extension Mastodon.Entity { public let url: String public let history: [History]? + enum CodingKeys: String, CodingKey { case name case url diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 4c76ebcf8..16c084cec 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 4c76ebcf8..16c084cec 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index ddb99a597..89e562534 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 NSExtension NSExtensionPointIdentifier diff --git a/NotificationService/MastodonNotification.swift b/NotificationService/MastodonNotification.swift index f3941b12d..7d6fb034d 100644 --- a/NotificationService/MastodonNotification.swift +++ b/NotificationService/MastodonNotification.swift @@ -9,7 +9,7 @@ import Foundation struct MastodonPushNotification: Codable { - private let _accessToken: String + let _accessToken: String var accessToken: String { return String.normalize(base64String: _accessToken) } @@ -32,4 +32,22 @@ struct MastodonPushNotification: Codable { case body } + public init( + _accessToken: String, + notificationID: Int, + notificationType: String, + preferredLocale: String?, + icon: String?, + title: String, + body: String + ) { + self._accessToken = _accessToken + self.notificationID = notificationID + self.notificationType = notificationType + self.preferredLocale = preferredLocale + self.icon = icon + self.title = title + self.body = body + } + } diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 08ddef3f3..79ba82cef 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.2.0 CFBundleVersion - 71 + 82 NSExtension NSExtensionAttributes diff --git a/ShareActionExtension/Scene/View/ComposeToolbarView.swift b/ShareActionExtension/Scene/View/ComposeToolbarView.swift index e6842c744..d88bb018c 100644 --- a/ShareActionExtension/Scene/View/ComposeToolbarView.swift +++ b/ShareActionExtension/Scene/View/ComposeToolbarView.swift @@ -190,7 +190,7 @@ extension ComposeToolbarView { extension ComposeToolbarView { private static func configureToolbarButtonAppearance(button: UIButton) { - button.tintColor = Asset.Colors.brandBlue.color + button.tintColor = ThemeService.tintColor button.setBackgroundImage(.placeholder(size: ComposeToolbarView.toolbarButtonSize, color: .systemFill), for: .highlighted) button.layer.masksToBounds = true button.layer.cornerRadius = 5 diff --git a/ShareActionExtension/Scene/View/ComposeView.swift b/ShareActionExtension/Scene/View/ComposeView.swift index 25adf4c5a..a688d6492 100644 --- a/ShareActionExtension/Scene/View/ComposeView.swift +++ b/ShareActionExtension/Scene/View/ComposeView.swift @@ -85,6 +85,7 @@ public struct ComposeView: View { .frame(height: viewModel.toolbarHeight + 20) .listRow(backgroundColor: Color(viewModel.backgroundColor)) } // end List + .listStyle(.plain) .introspectTableView(customize: { tableView in // tableView.keyboardDismissMode = .onDrag tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight @@ -101,7 +102,7 @@ public struct ComposeView: View { .introspectTableView(customize: { tableView in tableView.backgroundColor = .clear }) - .background(Color(viewModel.backgroundColor).ignoresSafeArea()) + .overrideBackground(color: Color(viewModel.backgroundColor)) } // end GeometryReader } // end body } @@ -112,10 +113,26 @@ struct ComposeListViewFramePreferenceKey: PreferenceKey { } extension View { + // hack for separator line + @ViewBuilder func listRow(backgroundColor: Color) -> some View { - self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) - .background(backgroundColor) + // expand list row to edge (set inset) + // then hide the separator + if #available(iOS 15, *) { + frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) + .background(backgroundColor) + .listRowSeparator(.hidden) // new API + } else { + frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) // separator line hidden magic + .background(backgroundColor) + } + } + + @ViewBuilder + func overrideBackground(color: Color) -> some View { + background(color.ignoresSafeArea()) } }