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())
}
}