diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ea365bca9..b2979d002 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -19,8 +19,8 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
- - name: force Xcode 13.0
- run: sudo xcode-select -switch /Applications/Xcode_13.0.app
+ - name: force Xcode 13.1
+ run: sudo xcode-select -switch /Applications/Xcode_13.1.app
- name: setup
run: exec ./.github/scripts/setup.sh
- name: build
diff --git a/.gitignore b/.gitignore
index 2c475f675..a605c524d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -122,5 +122,4 @@ xcuserdata
Localization/StringsConvertor/input
Localization/StringsConvertor/output
-.DS_Store
-/Mastodon.xcworkspace/xcshareddata/swiftpm
+.DS_Store
\ No newline at end of file
diff --git a/AppShared/Info.plist b/AppShared/Info.plist
index 16c084cec..9fe845c60 100644
--- a/AppShared/Info.plist
+++ b/AppShared/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
diff --git a/CoreDataStack/Info.plist b/CoreDataStack/Info.plist
index 16c084cec..9fe845c60 100644
--- a/CoreDataStack/Info.plist
+++ b/CoreDataStack/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
diff --git a/CoreDataStackTests/Info.plist b/CoreDataStackTests/Info.plist
index 16c084cec..9fe845c60 100644
--- a/CoreDataStackTests/Info.plist
+++ b/CoreDataStackTests/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
diff --git a/Localization/StringsConvertor/Intents/input/fr_FR/Intents.strings b/Localization/StringsConvertor/Intents/input/fr_FR/Intents.strings
index 628f794c9..f4fec3000 100644
--- a/Localization/StringsConvertor/Intents/input/fr_FR/Intents.strings
+++ b/Localization/StringsConvertor/Intents/input/fr_FR/Intents.strings
@@ -6,7 +6,7 @@
"HZSGTr" = "Quel contenu à publier ?";
-"HdGikU" = "Posting failed";
+"HdGikU" = "Échec lors de la publication";
"KDNTJ4" = "Raison de l’échec";
@@ -40,11 +40,11 @@
"gfePDu" = "Échec lors de la publication. ${failureReason}";
-"k7dbKQ" = "Post was sent successfully.";
+"k7dbKQ" = "Message publié avec succès.";
-"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?";
+"oGiqmY-dYQ5NN" = "Juste pour confirmer, vous vouliez « Public » ?";
-"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?";
+"oGiqmY-ehFLjY" = "Juste pour confirmer, vous vouliez bien diffuser vers « abonné·e·s uniquement » ?";
"rM6dvp" = "URL";
diff --git a/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings
index 3e1c69fc3..13a86e0c0 100644
--- a/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings
+++ b/Localization/StringsConvertor/Intents/input/kmr_TR/Intents.strings
@@ -24,15 +24,15 @@
"Zo4jgJ" = "Xuyaniya şandiyê";
-"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Giştî' ne.";
+"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Gelemperî' ne.";
-"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopandin' hene.";
+"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopaneran' hene.";
-"ayoYEb-dYQ5NN" = "${content}, Giştî";
+"ayoYEb-dYQ5NN" = "${content}, Gelemperî";
"ayoYEb-ehFLjY" = "${content}, Tenê şopînêr";
-"dUyuGg" = "Li ser Mastodon bişînin";
+"dUyuGg" = "Di Mastodon de biweşîne";
"dYQ5NN" = "Gelemperî";
@@ -42,10 +42,10 @@
"k7dbKQ" = "Şandî bi serkeftî hate şandin.";
-"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, we 'Giştî' dixwest?";
+"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, te 'Gelemperî' dixwest?";
-"oGiqmY-ehFLjY" = "Tenê ji bo piştrastkirinê, we 'Tenê Şopdarên' dixwest?";
+"oGiqmY-ehFLjY" = "Tenê ji bo pejirandinê, te 'Tenê Şopîner' dixwest?";
"rM6dvp" = "Girêdan";
-"ryJLwG" = "Bi serkeftî hat şandin. ";
+"ryJLwG" = "Şandî bi serkeftî hate şandin. ";
diff --git a/Localization/StringsConvertor/Intents/input/nl_NL/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/nl_NL/Intents.stringsdict
index 18422c772..9043720db 100644
--- a/Localization/StringsConvertor/Intents/input/nl_NL/Intents.stringsdict
+++ b/Localization/StringsConvertor/Intents/input/nl_NL/Intents.stringsdict
@@ -5,7 +5,7 @@
There are ${count} options matching ‘${content}’. - 2
NSStringLocalizedFormatKey
- There are %#@count_option@ matching ‘${content}’.
+ Er zijn %#@count_option@ die overeenkomen met ‘${content}’.
count_option
NSStringFormatSpecTypeKey
@@ -13,7 +13,7 @@
NSStringFormatValueTypeKey
%ld
one
- 1 option
+ 1 optie
other
%ld options
@@ -21,7 +21,7 @@
There are ${count} options matching ‘${visibility}’.
NSStringLocalizedFormatKey
- There are %#@count_option@ matching ‘${visibility}’.
+ Er zijn %#@count_option@ die overeenkomen met ‘${visibility}’.
count_option
NSStringFormatSpecTypeKey
@@ -29,7 +29,7 @@
NSStringFormatValueTypeKey
%ld
one
- 1 option
+ 1 optie
other
%ld options
diff --git a/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict b/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict
index e3dee0d80..0b28c577a 100644
--- a/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/ar_SA/Localizable.stringsdict
@@ -13,13 +13,13 @@
NSStringFormatValueTypeKey
ld
zero
- %ld unread notification
+ لا إشعار غير مقروء
one
إشعار واحِد غير مقروء
two
إشعاران غير مقروءان
few
- %ld unread notification
+ %ld إشعارات غير مقروءة
many
%ld إشعارًا غيرَ مقروء
other
@@ -277,7 +277,7 @@
NSStringFormatValueTypeKey
ld
zero
- %ld followers
+ لا مُتابِع
one
مُتابِعٌ واحد
two
diff --git a/Localization/StringsConvertor/input/ar_SA/app.json b/Localization/StringsConvertor/input/ar_SA/app.json
index 4bf55d918..71e0f538f 100644
--- a/Localization/StringsConvertor/input/ar_SA/app.json
+++ b/Localization/StringsConvertor/input/ar_SA/app.json
@@ -6,25 +6,25 @@
"please_try_again_later": "يُرجى المحاولة مرة أُخرى لاحقاً."
},
"sign_up_failure": {
- "title": "فشل التسجيل"
+ "title": "إخفاق في التسجيل"
},
"server_error": {
"title": "خطأ في الخادم"
},
"vote_failure": {
- "title": "فشل التصويت",
+ "title": "إخفاق في التصويت",
"poll_ended": "انتهى استطلاع الرأي"
},
"discard_post_content": {
- "title": "تجاهل المسودة",
- "message": "Confirm to discard composed post content."
+ "title": "التخلص من المسودة",
+ "message": "أكِّد للتخلص مِن مُحتوى مَنشور مؤلَّف."
},
"publish_post_failure": {
- "title": "أخفقت عملية النشر",
- "message": "Failed to publish the post.\nPlease check your internet connection.",
+ "title": "إخفاق في عمليَّة النشر",
+ "message": "فَشَلَ نَشر المَنشور.\nيُرجى التحقق من اتصالك بالإنترنت.",
"attachments_message": {
- "video_attach_with_photo": "Cannot attach a video to a post that already contains images.",
- "more_than_one_video": "Cannot attach more than one video."
+ "video_attach_with_photo": "لا يُمكن إرفاق مقطع مرئي إلى مَنشور يحتوي بالفعل على صُوَر.",
+ "more_than_one_video": "لا يُمكِنُ إرفاق أكثر مِن مَقطع مرئي واحِد."
}
},
"edit_profile_failure": {
@@ -33,19 +33,19 @@
},
"sign_out": {
"title": "تسجيل الخروج",
- "message": "هل أنت متأكد من أنك تريد تسجيل الخروج؟",
+ "message": "هل أنت متأكد من رغبتك في تسجيل الخروج؟",
"confirm": "تسجيل الخروج"
},
"block_domain": {
- "title": "Are you really, really sure you want to block the entire %s? 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.",
- "block_entire_domain": "حظر النطاق"
+ "title": "هل أنتَ مُتأكِّدٌ حقًا مِن رغبتك في حظر %s بالكامل؟ في معظم الحالات، يكون مِنَ الكافي والمُفَضَّل استهداف عدد محدود للحظر أو الكتم. لن ترى محتوى من هذا النطاق وسوف يتم إزالة جميع متابعيك المتواجدين فيه.",
+ "block_entire_domain": "حظر النِطاق"
},
"save_photo_failure": {
- "title": "فشل حفظ الصورة",
- "message": "Please enable the photo library access permission to save the photo."
+ "title": "إخفاق في حفظ الصورة",
+ "message": "يُرجى إتاحة إذن الوصول إلى مكتبة الصور لحفظ الصورة."
},
"delete_post": {
- "title": "هل أنت متأكد من أنك تريد حذف هذا المنشور؟",
+ "title": "هل أنت متأكد من رغبتك في حذف هذا المنشور؟",
"delete": "احذف"
},
"clean_cache": {
@@ -61,30 +61,31 @@
"open": "افتح",
"add": "إضافة",
"remove": "احذف",
- "edit": "تعديل",
+ "edit": "تحرير",
"save": "حفظ",
"ok": "حسنًا",
"done": "تمّ",
"confirm": "تأكيد",
"continue": "واصل",
+ "compose": "تأليف",
"cancel": "إلغاء",
"discard": "تجاهل",
"try_again": "المُحاولة مرة أُخرى",
- "take_photo": "التقط صورة",
+ "take_photo": "التقاط صورة",
"save_photo": "حفظ الصورة",
"copy_photo": "نسخ الصورة",
"sign_in": "تسجيل الدخول",
"sign_up": "إنشاء حِساب",
"see_more": "عرض المزيد",
- "preview": "معاينة",
- "share": "شارك",
- "share_user": "شارك %s",
- "share_post": "شارك المنشور",
- "open_in_safari": "افتحه في سفاري",
+ "preview": "مُعاينة",
+ "share": "المُشارك",
+ "share_user": "مُشاركة %s",
+ "share_post": "مشارك المنشور",
+ "open_in_safari": "الفتح في Safari",
"find_people": "ابحث عن أشخاص لمتابعتهم",
"manually_search": "البحث يدوياً بدلاً من ذلك",
"skip": "تخطي",
- "reply": "رد",
+ "reply": "الرَد",
"report_user": "ابلغ عن %s",
"block_domain": "حظر %s",
"unblock_domain": "إلغاء حظر %s",
@@ -100,7 +101,7 @@
"keyboard": {
"common": {
"switch_to_tab": "التبديل إلى %s",
- "compose_new_post": "إنشاء منشور جديد",
+ "compose_new_post": "تأليف منشور جديد",
"show_favorites": "إظهار المفضلة",
"open_settings": "أفتح الإعدادات"
},
@@ -111,9 +112,9 @@
"open_author_profile": "افتح الملف التعريفي للمؤلف",
"open_reblogger_profile": "افتح الملف التعريفي لمشارِك المنشور",
"reply_status": "رد على المنشور",
- "toggle_reblog": "Toggle Reblog on Post",
- "toggle_favorite": "Toggle Favorite on Post",
- "toggle_content_warning": "Toggle Content Warning",
+ "toggle_reblog": "تبديل إعادة تدوين منشور",
+ "toggle_favorite": "تبديل المفضلة لِمنشور",
+ "toggle_content_warning": "تبديل تحذير المُحتَوى",
"preview_image": "معاينة الصورة"
},
"segmented_control": {
@@ -127,9 +128,9 @@
"show_post": "اظهر المنشور",
"show_user_profile": "اظهر الملف التعريفي للمستخدم",
"content_warning": "تحذير عن المحتوى",
- "media_content_warning": "Tap anywhere to reveal",
+ "media_content_warning": "انقر على أي مكان للكشف",
"poll": {
- "vote": "صَوّت",
+ "vote": "صَوِّت",
"closed": "انتهى"
},
"actions": {
@@ -179,19 +180,19 @@
},
"header": {
"no_status_found": "لا توجد هناك منشورات",
- "blocking_warning": "You can’t view this user's profile\nuntil you unblock them.\nYour profile looks like this to them.",
- "user_blocking_warning": "You can’t view %s’s profile\nuntil you unblock them.\nYour profile looks like this to them.",
- "blocked_warning": "You can’t view this user’s profile\nuntil they unblock you.",
- "user_blocked_warning": "You can’t view %s’s profile\nuntil they unblock you.",
- "suspended_warning": "This user has been suspended.",
- "user_suspended_warning": "%s’s account has been suspended."
+ "blocking_warning": "لا يُمكنك الاطلاع على الملف الشخصي لهذا المُستخدِم\nحتَّى تَرفعَ الحَظر عنه.\nملفًّكَ الشخصي يَظهَرُ بِمثل هذِهِ الحالة بالنسبةِ لَهُ أيضًا.",
+ "user_blocking_warning": "لا يُمكنك الاطلاع على ملف %s الشخصي\nحتَّى تَرفعَ الحَظر عنه.\nملفًّكَ الشخصي يَظهَرُ بِمثل هذِهِ الحالة بالنسبةِ لَهُ أيضًا.",
+ "blocked_warning": "لا يُمكِنُكَ عَرض الملف الشخصي لهذا المُستخدِم\nحتَّى يَرفَعَ الحَظر عَنك.",
+ "user_blocked_warning": "لا يُمكِنُكَ عَرض ملف %s الشخصي\nحتَّى يَرفَعَ الحَظر عَنك.",
+ "suspended_warning": "تمَّ إيقاف هذا المُستخدِم.",
+ "user_suspended_warning": "لقد أوقِفَ حِساب %s."
}
}
}
},
"scene": {
"welcome": {
- "slogan": "Social networking\nback in your hands."
+ "slogan": "شبكات التواصل الاجتماعي\nمرة أُخرى بين يديك."
},
"server_picker": {
"title": "اِختر خادِم،\nأي خادِم.",
@@ -225,7 +226,7 @@
},
"empty_state": {
"finding_servers": "البحث عن خوادم متوفرة...",
- "bad_network": "Something went wrong while loading the data. Check your internet connection.",
+ "bad_network": "حدث خطأٌ ما أثناء تحميل البيانات. تحقَّق من اتصالك بالإنترنت.",
"no_results": "لا توجد نتائج"
}
},
@@ -263,20 +264,20 @@
"reason": "السبب"
},
"reason": {
- "blocked": "%s contains a disallowed email provider",
- "unreachable": "%s does not seem to exist",
- "taken": "%s is already in use",
- "reserved": "%s is a reserved keyword",
- "accepted": "%s must be accepted",
+ "blocked": "يحتوي %s على موفِّر خدمة بريد إلكتروني غير مسموح به",
+ "unreachable": "يبدوا أنَّ %s غير موجود",
+ "taken": "إنَّ %s مُستخدَمٌ بالفعل",
+ "reserved": "إنَّ %s عبارة عن كلمة مفتاحيَّة محجوزة",
+ "accepted": "يجب أن يُقبل %s",
"blank": "%s مطلوب",
"invalid": "%s غير صالح",
"too_long": "%s طويل جداً",
"too_short": "%s قصير جدا",
- "inclusion": "%s is not a supported value"
+ "inclusion": "إنَّ %s قيمة غير مدعومة"
},
"special": {
- "username_invalid": "Username must only contain alphanumeric characters and underscores",
- "username_too_long": "Username is too long (can’t be longer than 30 characters)",
+ "username_invalid": "يُمكِن أن يحتوي اسم المستخدم على أحرف أبجدية، أرقام وشرطات سفلية فقط",
+ "username_too_long": "اسم المستخدم طويل جداً (يجب ألّا يكون أطول من 30 رمز)",
"email_invalid": "هذا عنوان بريد إلكتروني غير صالح",
"password_too_short": "كلمة المرور قصيرة جداً (يجب أن تكون 8 أحرف على الأقل)"
}
@@ -301,12 +302,12 @@
},
"dont_receive_email": {
"title": "تحقق من بريدك الإلكتروني",
- "description": "Check if your email address is correct as well as your junk folder if you haven’t.",
+ "description": "تحقق ممَّ إذا كان عنوان بريدك الإلكتروني صحيحًا وكذلك تأكد مِن مجلد البريد غير الهام إذا لم تكن قد فعلت ذلك.",
"resend_email": "إعادة إرسال البريد الإلكتروني"
},
"open_email_app": {
"title": "تحقَّق من بريدك الوارِد.",
- "description": "We just sent you an email. Check your junk folder if you haven’t.",
+ "description": "لقد أرسلنا لك بريدًا إلكترونيًا للتو. تحقق من مجلد البريد غير الهام الخاص بك إذا لم تكن قد فعلت ذلك.",
"mail": "البريد",
"open_email_client": "فتح عميل البريد الإلكتروني"
}
@@ -322,7 +323,7 @@
},
"suggestion_account": {
"title": "ابحث عن أشخاص لمتابعتهم",
- "follow_explain": "When you follow someone, you’ll see their posts in your home feed."
+ "follow_explain": "عِندَ مُتابَعَتِكَ لأحدِهِم، سَوف تَرى مَنشوراته في تغذيَتِكَ الرئيسة."
},
"compose": {
"title": {
@@ -340,9 +341,9 @@
"attachment": {
"photo": "صورة",
"video": "فيديو",
- "attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
- "description_photo": "Describe the photo for the visually-impaired...",
- "description_video": "Describe the video for the visually-impaired..."
+ "attachment_broken": "هذا ال%s مُعطَّل ويتعذَّر رفعه إلى ماستودون.",
+ "description_photo": "صِف الصورة للمكفوفين...",
+ "description_video": "صِف المقطع المرئي للمكفوفين..."
},
"poll": {
"duration_time": "المدة: %s",
@@ -355,7 +356,7 @@
"option_number": "الخيار %ld"
},
"content_warning": {
- "placeholder": "Write an accurate warning here..."
+ "placeholder": "اكتب تَحذيرًا دَقيقًا هُنا..."
},
"visibility": {
"public": "للعامة",
@@ -364,7 +365,7 @@
"direct": "ففط للأشخاص المشار إليهم"
},
"auto_complete": {
- "space_to_add": "Space to add"
+ "space_to_add": "انقر مساحة لإضافتِها"
},
"accessibility": {
"append_attachment": "إضافة مُرفَق",
@@ -373,7 +374,7 @@
"custom_emoji_picker": "منتقي مخصص للإيموجي",
"enable_content_warning": "تنشيط تحذير المحتوى",
"disable_content_warning": "تعطيل تحذير الحتوى",
- "post_visibility_menu": "Post Visibility Menu"
+ "post_visibility_menu": "قائمة ظهور المنشور"
},
"keyboard": {
"discard_post": "تجاهُل المنشور",
@@ -405,14 +406,20 @@
"relationship_action_alert": {
"confirm_unmute_user": {
"title": "إلغاء كتم الحساب",
- "message": "Confirm to unmute %s"
+ "message": "أكِّد لرفع كتمْ %s"
},
"confirm_unblock_usre": {
"title": "إلغاء حظر الحساب",
- "message": "Confirm to unblock %s"
+ "message": "أكِّد لرفع حظر %s"
}
}
},
+ "follower": {
+ "footer": "لا يُمكِن عَرض المُتابِعين مِنَ الخوادم الأُخرى."
+ },
+ "following": {
+ "footer": "لا يُمكِن عَرض المُتابَعات مِنَ الخوادم الأُخرى."
+ },
"search": {
"title": "بحث",
"search_bar": {
@@ -423,7 +430,7 @@
"button_text": "طالع الكل",
"hash_tag": {
"title": "ذات شعبية على ماستدون",
- "description": "Hashtags that are getting quite a bit of attention",
+ "description": "الوسوم التي تحظى بقدر كبير من الاهتمام",
"people_talking": "%s أشخاص يتحدَّثوا"
},
"accounts": {
@@ -443,7 +450,7 @@
"no_results": "ليس هناك أية نتيجة"
},
"recent_search": "عمليات البحث الأخيرة",
- "clear": "امسح"
+ "clear": "مَحو"
}
},
"favorite": {
@@ -512,7 +519,7 @@
}
},
"footer": {
- "mastodon_description": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على غيت هب %s (%s)"
+ "mastodon_description": "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء على GitHub في %s (%s)"
},
"keyboard": {
"close_settings_window": "إغلاق نافذة الإعدادات"
@@ -522,27 +529,27 @@
"title": "ابلغ عن %s",
"step1": "الخطوة 1 من 2",
"step2": "الخطوة 2 من 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": "ارسل الشكوى",
+ "content1": "هل ترغب في إضافة أي مشاركات أُخرى إلى الشكوى؟",
+ "content2": "هل هناك أي شيء يجب أن يعرفه المُراقبين حول هذه الشكوى؟",
+ "send": "إرسال الشكوى",
"skip_to_send": "إرسال بدون تعليق",
- "text_placeholder": "Type or paste additional comments"
+ "text_placeholder": "اكتب أو الصق تعليقات إضافيَّة"
},
"preview": {
"keyboard": {
- "close_preview": "إغلاق المعاينة",
+ "close_preview": "إغلاق المُعايَنَة",
"show_next": "إظهار التالي",
"show_previous": "إظهار السابق"
}
},
"account_list": {
- "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher",
+ "tab_bar_hint": "المِلف المُحدَّد حاليًا: %s. انقر نقرًا مزدوجًا ثم اضغط مع الاستمرار لإظهار مُبدِّل الحِساب",
"dismiss_account_switcher": "تجاهُل مبدِّل الحساب",
"add_account": "إضافة حساب"
},
"wizard": {
"new_in_mastodon": "جديد في ماستودون",
- "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.",
+ "multiple_account_switch_intro_description": "بدِّل بين حسابات متعددة عبر الاستمرار بالضغط على زر الملف الشخصي.",
"accessibility_hint": "انقر نقرًا مزدوجًا لتجاهل النافذة المنبثقة"
}
}
diff --git a/Localization/StringsConvertor/input/ca_ES/app.json b/Localization/StringsConvertor/input/ca_ES/app.json
index 9ffdc5bee..2ecd587c6 100644
--- a/Localization/StringsConvertor/input/ca_ES/app.json
+++ b/Localization/StringsConvertor/input/ca_ES/app.json
@@ -67,6 +67,7 @@
"done": "Fet",
"confirm": "Confirma",
"continue": "Continua",
+ "compose": "Composa",
"cancel": "Cancel·la",
"discard": "Descarta",
"try_again": "Torna a provar",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Els seguidors d'altres servidors no son mostrats."
+ },
+ "following": {
+ "footer": "Els seguits d'altres servidors no son mostrats."
+ },
"search": {
"title": "Cerca",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/cy_GB/app.json b/Localization/StringsConvertor/input/cy_GB/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/cy_GB/app.json
+++ b/Localization/StringsConvertor/input/cy_GB/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/Localization/StringsConvertor/input/da_DK/app.json b/Localization/StringsConvertor/input/da_DK/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/da_DK/app.json
+++ b/Localization/StringsConvertor/input/da_DK/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/Localization/StringsConvertor/input/de_DE/app.json b/Localization/StringsConvertor/input/de_DE/app.json
index 43d8ed70a..dc8cdf8c0 100644
--- a/Localization/StringsConvertor/input/de_DE/app.json
+++ b/Localization/StringsConvertor/input/de_DE/app.json
@@ -67,6 +67,7 @@
"done": "Fertig",
"confirm": "Bestätigen",
"continue": "Fortfahren",
+ "compose": "Compose",
"cancel": "Abbrechen",
"discard": "Verwerfen",
"try_again": "Nochmals versuchen",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Followers from other servers are not displayed."
+ },
+ "following": {
+ "footer": "Follows from other servers are not displayed."
+ },
"search": {
"title": "Suche",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/en_US/app.json b/Localization/StringsConvertor/input/en_US/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/en_US/app.json
+++ b/Localization/StringsConvertor/input/en_US/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/Localization/StringsConvertor/input/es_AR/app.json b/Localization/StringsConvertor/input/es_AR/app.json
index 4ae708521..ed909ecf1 100644
--- a/Localization/StringsConvertor/input/es_AR/app.json
+++ b/Localization/StringsConvertor/input/es_AR/app.json
@@ -67,6 +67,7 @@
"done": "Listo",
"confirm": "Confirmar",
"continue": "Continuar",
+ "compose": "Redactar",
"cancel": "Cancelar",
"discard": "Descartar",
"try_again": "Intentá de nuevo",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "No se muestran los seguidores de otros servidores."
+ },
+ "following": {
+ "footer": "No se muestran las cuentas de otros servidores que seguís."
+ },
"search": {
"title": "Buscar",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/es_ES/app.json b/Localization/StringsConvertor/input/es_ES/app.json
index 5e96daff0..72967c40f 100644
--- a/Localization/StringsConvertor/input/es_ES/app.json
+++ b/Localization/StringsConvertor/input/es_ES/app.json
@@ -67,6 +67,7 @@
"done": "Hecho",
"confirm": "Confirmar",
"continue": "Continuar",
+ "compose": "Redactar",
"cancel": "Cancelar",
"discard": "Descartar",
"try_again": "Inténtalo de nuevo",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "No se muestran los seguidores de otros servidores."
+ },
+ "following": {
+ "footer": "No se muestran los seguidos de otros servidores."
+ },
"search": {
"title": "Buscar",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/fr_FR/Localizable.stringsdict b/Localization/StringsConvertor/input/fr_FR/Localizable.stringsdict
index d512b204c..4a912e4b3 100644
--- a/Localization/StringsConvertor/input/fr_FR/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/fr_FR/Localizable.stringsdict
@@ -13,15 +13,15 @@
NSStringFormatValueTypeKey
ld
one
- 1 unread notification
+ 1 notification non lue
other
- %ld unread notification
+ %ld notifications non lues
a11y.plural.count.input_limit_exceeds
NSStringLocalizedFormatKey
- Input limit exceeds %#@character_count@
+ La limite d’entrée dépasse %#@character_count@
character_count
NSStringFormatSpecTypeKey
diff --git a/Localization/StringsConvertor/input/fr_FR/app.json b/Localization/StringsConvertor/input/fr_FR/app.json
index 5c101dbe9..dd834928a 100644
--- a/Localization/StringsConvertor/input/fr_FR/app.json
+++ b/Localization/StringsConvertor/input/fr_FR/app.json
@@ -67,6 +67,7 @@
"done": "Terminé",
"confirm": "Confirmer",
"continue": "Continuer",
+ "compose": "Compose",
"cancel": "Annuler",
"discard": "Abandonner",
"try_again": "Réessayer",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Les abonné·e·s issus des autres serveurs ne sont pas affiché·e·s."
+ },
+ "following": {
+ "footer": "Follows from other servers are not displayed."
+ },
"search": {
"title": "Rechercher",
"search_bar": {
@@ -538,12 +545,12 @@
"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": "Ajouter un compte"
},
"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": "Nouveau dans Mastodon",
+ "multiple_account_switch_intro_description": "Basculez entre plusieurs comptes en appuyant de maniere prolongée sur le bouton profil.",
+ "accessibility_hint": "Tapotez deux fois pour fermer cet assistant"
}
}
}
\ No newline at end of file
diff --git a/Localization/StringsConvertor/input/gd_GB/app.json b/Localization/StringsConvertor/input/gd_GB/app.json
index a73925bba..b5c66f8f6 100644
--- a/Localization/StringsConvertor/input/gd_GB/app.json
+++ b/Localization/StringsConvertor/input/gd_GB/app.json
@@ -67,6 +67,7 @@
"done": "Deiseil",
"confirm": "Dearbh",
"continue": "Lean air adhart",
+ "compose": "Sgrìobh",
"cancel": "Sguir dheth",
"discard": "Tilg air falbh",
"try_again": "Feuch ris a-rithist",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Cha dèid luchd-leantainn o fhrithealaichean eile a shealltainn."
+ },
+ "following": {
+ "footer": "Cha dèid cò air a leanas tu air frithealaichean eile a shealltainn."
+ },
"search": {
"title": "Lorg",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/hi_IN/app.json b/Localization/StringsConvertor/input/hi_IN/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/hi_IN/app.json
+++ b/Localization/StringsConvertor/input/hi_IN/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/Localization/StringsConvertor/input/id_ID/app.json b/Localization/StringsConvertor/input/id_ID/app.json
index 6ba29ad8f..6f3171254 100644
--- a/Localization/StringsConvertor/input/id_ID/app.json
+++ b/Localization/StringsConvertor/input/id_ID/app.json
@@ -67,6 +67,7 @@
"done": "Selesai",
"confirm": "Konfirmasi",
"continue": "Lanjut",
+ "compose": "Compose",
"cancel": "Batal",
"discard": "Discard",
"try_again": "Coba Lagi",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Followers from other servers are not displayed."
+ },
+ "following": {
+ "footer": "Follows from other servers are not displayed."
+ },
"search": {
"title": "Cari",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/ja_JP/app.json b/Localization/StringsConvertor/input/ja_JP/app.json
index 1c7d408f5..417ca3e3a 100644
--- a/Localization/StringsConvertor/input/ja_JP/app.json
+++ b/Localization/StringsConvertor/input/ja_JP/app.json
@@ -67,6 +67,7 @@
"done": "完了",
"confirm": "確認",
"continue": "続ける",
+ "compose": "Compose",
"cancel": "キャンセル",
"discard": "破棄",
"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_bar": {
diff --git a/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict b/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict
index 064b8bf2b..8ae1b812a 100644
--- a/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/kmr_TR/Localizable.stringsdict
@@ -109,9 +109,9 @@
NSStringFormatValueTypeKey
ld
one
- 1 reblog
+ 1 ji nû ve nivîsandin
other
- %ld reblogs
+ %ld ji nû ve nivîsandin
plural.count.vote
diff --git a/Localization/StringsConvertor/input/kmr_TR/app.json b/Localization/StringsConvertor/input/kmr_TR/app.json
index 9798c86c2..c360eb430 100644
--- a/Localization/StringsConvertor/input/kmr_TR/app.json
+++ b/Localization/StringsConvertor/input/kmr_TR/app.json
@@ -16,8 +16,8 @@
"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."
+ "title": "Reşnivîsê paşguh bike",
+ "message": "Bipejrîne ku naveroka şandiyê ya hatiye nivîsandin paşguh bikî."
},
"publish_post_failure": {
"title": "Weşandin têkçû",
@@ -28,7 +28,7 @@
}
},
"edit_profile_failure": {
- "title": "Çewtiya profîlê biguherîne",
+ "title": "Di serrastkirina profîlê çewtî",
"message": "Nikare profîlê serrast bike. Jkx dîsa biceribîne."
},
"sign_out": {
@@ -42,15 +42,15 @@
},
"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."
+ "message": "Ji kerema xwe mafê bide gihîştina wênegehê çalak bike da ku wêne werin tomarkirin."
},
"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."
+ "title": "Pêşbîrê pak bike",
+ "message": "Pêşbîra %s biserketî hate pakkirin."
}
},
"controls": {
@@ -67,12 +67,13 @@
"done": "Qediya",
"confirm": "Bipejirîne",
"continue": "Bidomîne",
+ "compose": "Binivîsî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",
+ "save_photo": "Wêneyê tomar bike",
+ "copy_photo": "Wêneyê jê bigire",
"sign_in": "Têkeve",
"sign_up": "Tomar bibe",
"see_more": "Bêtir bibîne",
@@ -81,8 +82,8 @@
"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",
+ "find_people": "Mirovan bo şopandinê bibîne",
+ "manually_search": "Ji devlê bi destan lêgerînê bike",
"skip": "Derbas bike",
"reply": "Bersivê bide",
"report_user": "%s ragihîne",
@@ -111,48 +112,48 @@
"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ê"
+ "toggle_reblog": "Ji vû nivîsandin di şandiyê de biguherîne",
+ "toggle_favorite": "Li ser şandiyê bijarte biguherîne",
+ "toggle_content_warning": "Hişyariya naverokê biguherîne",
+ "preview_image": "Pêşdîtina wêneyê"
},
"segmented_control": {
- "previous_section": "Beşa berê",
- "next_section": "Beşa paşê"
+ "previous_section": "Beşa paş",
+ "next_section": "Beşa pêş"
}
},
"status": {
- "user_reblogged": "%s ji nû ve hat blogkirin",
+ "user_reblogged": "%s ji nû ve hate nivîsandin",
"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",
+ "media_content_warning": "Ji bo eşkerekirinê li derekî bitikîne",
"poll": {
- "vote": "Deng",
+ "vote": "Deng bide",
"closed": "Girtî"
},
"actions": {
"reply": "Bersivê bide",
- "reblog": "Ji nû ve blog",
- "unreblog": "Ji nû ve blogkirin betal bikin",
- "favorite": "Bijartî",
- "unfavorite": "Nebijare",
- "menu": "Menû"
+ "reblog": "Ji nû ve nivîsandin",
+ "unreblog": "Ji nû ve nivîsandinê vegere",
+ "favorite": "Bijarte",
+ "unfavorite": "Nebijarte",
+ "menu": "Kulîn"
},
"tag": {
"url": "URL",
- "mention": "Behs",
+ "mention": "Qalkirin",
"link": "Girêdan",
- "hashtag": "Etîket",
+ "hashtag": "Hashtag",
"email": "E-name",
- "emoji": "E-name"
+ "emoji": "Emojî"
}
},
"friendship": {
"follow": "Bişopîne",
"following": "Dişopîne",
- "request": "Daxwazên şopandinê",
+ "request": "Daxwaz bike",
"pending": "Tê nirxandin",
"block": "Asteng bike",
"block_user": "%s asteng bike",
@@ -173,18 +174,18 @@
"now": "Niha"
},
"loader": {
- "load_missing_posts": "Barkirina posteyên kêm",
- "loading_missing_posts": "Barkirina posteyên kêm...",
+ "load_missing_posts": "Şandiyên wendayî bar bike",
+ "loading_missing_posts": "Şandiyên wendayî tên barkirin...",
"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.",
+ "no_status_found": "Tu şandî nehate dîtin",
+ "blocking_warning": "Tu nikarî profîla vî/ê bikarhênerî bibînî\nHeya ku tu astengiyê li ser wî/ê ranekî.\nProfîla te ji wan ra wiha xuya dike.",
+ "user_blocking_warning": "Tu nikarî profîla %s bibînî\nHeya ku tu astengiyê li ser wî/ê ranekî.\nProfîla te ji wan ra wiha xuya dike.",
+ "blocked_warning": "Tu nikarî profîla vî/ê bikarhênerî bibînî\nheya ku ew astengiyê li ser 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."
+ "suspended_warning": "Ev bikarhêner hatiye rawestandin.",
+ "user_suspended_warning": "Ajimêra %s hatiye rawestandin."
}
}
}
@@ -218,15 +219,15 @@
"label": {
"language": "ZIMAN",
"users": "BIKARHÊNER",
- "category": "KATEGORÎ"
+ "category": "BEŞ"
},
"input": {
- "placeholder": "Serverek bibînin an jî beşdarî ya xwe bibin..."
+ "placeholder": "Rajekarekî bibîne an jî beşdarî ya xwe bibe..."
},
"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"
+ "finding_servers": "Peydakirina rajekarên berdest...",
+ "bad_network": "Di dema barkirina daneyan da çewtî derket. Girêdana xwe ya înternetê kontrol bike.",
+ "no_results": "Encam tune"
}
},
"register": {
@@ -246,8 +247,8 @@
"placeholder": "e-name"
},
"password": {
- "placeholder": "şîfre",
- "hint": "Şîfreya we herî kêm heşt tîpan hewce dike"
+ "placeholder": "pêborîn",
+ "hint": "Pêborîna te herî kêm divê ji 8 tîpan pêk bê"
},
"invite": {
"registration_user_invite_request": "Tu çima dixwazî beşdar bibî?"
@@ -257,44 +258,44 @@
"item": {
"username": "Navê bikarhêner",
"email": "E-name",
- "password": "Şîfre",
- "agreement": "Lihevhatin",
- "locale": "Herêm",
+ "password": "Pêborîn",
+ "agreement": "Peyman",
+ "locale": "Zimanê navrûyê",
"reason": "Sedem"
},
"reason": {
- "blocked": "%s peydekerê e-nameya bêdestûr dihewîne",
- "unreachable": "%s xuya nake",
+ "blocked": "%s peydekerê e-peyamê yê qedexekirî dihewîne",
+ "unreachable": "%s xuya ye ku tune ye",
"taken": "%s jixwe tê bikaranîn",
- "reserved": "%s peyveke mifteya veqetandî ye",
- "accepted": "%s divê were qebûlkirin",
+ "reserved": "%s peyveke parastî ye",
+ "accepted": "%s divê were pejirandin",
"blank": "%s pêwist e",
"invalid": "%s ne derbasdar e",
- "too_long": "%s gelekî dirêj e",
+ "too_long": "%s pir dirêj e",
"too_short": "%s pir kurt e",
- "inclusion": "%s nirxeke ku tê destekirin nîn e"
+ "inclusion": "%s ne nirxek piştgirî ye"
},
"special": {
- "username_invalid": "Navê bikarhêner divê tenê tîpên alfanumerîk û binxet hebe",
+ "username_invalid": "Navê bikarhêner divê tenê ji tîpên alfajimarî û binxêz pêk be",
"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)"
+ "email_invalid": "Ev navnîşaneke e-nameyê ne derbasdar e",
+ "password_too_short": "Pêborîn pir kurt e (divê herî kêm 8 tîp be)"
}
}
},
"server_rules": {
- "title": "Hin qaîdeyên bingehîn.",
+ "title": "Hinek rêzikê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ê",
+ "prompt": "Bi domandinê, tu ji bo %s di bin mercên bikaranînê û polîtîkaya nepenîtiyê dipejirînî.",
+ "terms_of_service": "mercên bikaranînê",
+ "privacy_policy": "polîtikaya nihêniyê",
"button": {
- "confirm": "Ez tev dibim"
+ "confirm": "Ez dipejirînim"
}
},
"confirm_email": {
"title": "Tiştekî dawî.",
- "subtitle": "We just sent an email to %s,\ntap the link to confirm your account.",
+ "subtitle": "Me tenê e-nameyek ji %s re şand,\ngirêdanê bitikne da ku ajimêra xwe bidî piştrastkirin.",
"button": {
"open_email_app": "Sepana e-nameyê veke",
"dont_receive_email": "Min hîç e-nameyeke nesitand"
@@ -305,8 +306,8 @@
"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.",
+ "title": "Nameyên xwe yên wergirtî kontrol bike.",
+ "description": "Me tenê ji te re e-nameyek şand. Heke nehatiye peldanka xwe ya spamê kontrol bike.",
"mail": "E-name",
"open_email_client": "Rajegirê e-nameyê veke"
}
@@ -334,15 +335,15 @@
"photo_library": "Wênegeh",
"browse": "Bigere"
},
- "content_input_placeholder": "Type or paste what’s on your mind",
+ "content_input_placeholder": "Tiştê ku di hişê te de ye binivîsin an jî pêve bike",
"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..."
+ "description_photo": "Wêneyê ji bo kêmbînên dîtbar bide nasîn...",
+ "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn..."
},
"poll": {
"duration_time": "Dirêjî: %s",
@@ -355,7 +356,7 @@
"option_number": "Vebijêrk %ld"
},
"content_warning": {
- "placeholder": "Write an accurate warning here..."
+ "placeholder": "Li vir hişyariyek hûrgilî binivîsine..."
},
"visibility": {
"public": "Gelemperî",
@@ -364,71 +365,77 @@
"direct": "Tenê mirovên ku min qalkirî"
},
"auto_complete": {
- "space_to_add": "Space to add"
+ "space_to_add": "Bicîhkirinê tevlî bike"
},
"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",
+ "custom_emoji_picker": "Hilbijêrê emojî yên kesanekirî",
+ "enable_content_warning": "Hişyariya naverokê çalak bike",
"disable_content_warning": "Hişyariya naverokê neçalak bike",
- "post_visibility_menu": "Menuya Xuyabûna Şandiyê"
+ "post_visibility_menu": "Kulîna 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"
+ "discard_post": "Şandî paşguh bike",
+ "publish_post": "Şandiyê biweşîne",
+ "toggle_poll": "Rapirsiyê biguherîne",
+ "toggle_content_warning": "Hişyariya naverokê biguherîne",
+ "append_attachment_entry": "Pêvek tevlî bike - %s",
+ "select_visibility_entry": "Xuyabûnê hilbijêre - %s"
}
},
"profile": {
"dashboard": {
- "posts": "şandîyan",
+ "posts": "şandî",
"following": "dişopîne",
- "followers": "şopîneran"
+ "followers": "şopîner"
},
"fields": {
- "add_row": "Rêzê lê zêde bike",
+ "add_row": "Rêzê tevlî bike",
"placeholder": {
"label": "Nîşan",
"content": "Naverok"
}
},
"segmented_control": {
- "posts": "Şandîyan",
- "replies": "Bersivan",
+ "posts": "Şandî",
+ "replies": "Bersiv",
"media": "Medya"
},
"relationship_action_alert": {
"confirm_unmute_user": {
- "title": "Hesabê ji bê deng rake",
- "message": "Ji bo vekirina bê dengkirinê bipejirin %s"
+ "title": "Ajimêrê bêdeng neke",
+ "message": "Ji bo vekirina bêdengkirinê bipejirîne %s"
},
"confirm_unblock_usre": {
- "title": "Hesabê ji bloke rake",
- "message": "Ji bo rakirina blokê bipejirin %s"
+ "title": "Astengiyê li ser ajimêr rake",
+ "message": "Ji bo rakirina astengkirinê bipejirîne %s"
}
}
},
+ "follower": {
+ "footer": "Şopîner ji rajekerên din nayê dîtin."
+ },
+ "following": {
+ "footer": "Şopandin ji rajekerên din nayê dîtin."
+ },
"search": {
"title": "Bigere",
"search_bar": {
- "placeholder": "Li etîketan û bikarhêneran bigerin",
- "cancel": "Betal kirin"
+ "placeholder": "Li hashtag û bikarhêneran bigere",
+ "cancel": "Dev jê berde"
},
"recommend": {
- "button_text": "Hemûyé bibîne",
+ "button_text": "Hemûyan bibîne",
"hash_tag": {
- "title": "Trend li ser Mastodon",
- "description": "Etîketên ku pir balê dikişînin",
+ "title": "Rojev li ser Mastodon",
+ "description": "Hashtag ê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î",
+ "title": "Ajimêrên ku belkî tu jê hez bikî",
+ "description": "Dibe ku tu bixwazî van ajimêran bişopînî",
"follow": "Bişopîne"
}
},
@@ -436,38 +443,38 @@
"segment": {
"all": "Hemû",
"people": "Mirov",
- "hashtags": "Etîketan",
- "posts": "Şandîyan"
+ "hashtags": "Hashtag",
+ "posts": "Şandî"
},
"empty_state": {
"no_results": "Encam tune"
},
"recent_search": "Lêgerînên dawî",
- "clear": "Paqij bike"
+ "clear": "Pak bike"
}
},
"favorite": {
- "title": "Bijareyên te"
+ "title": "Bijarteyên te"
},
"notification": {
"title": {
"Everything": "Her tişt",
- "Mentions": "Behs"
+ "Mentions": "Qalkirin"
},
"user_followed_you": "%s te şopand",
- "user_favorited your post": "%s posta we bijarte",
+ "user_favorited your post": "%s şandiya te hez kir",
"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",
+ "user_mentioned_you": "%s qale te kir",
+ "user_requested_to_follow_you": "%s dixwazê te bişopîne",
+ "user_your_poll_has_ended": "Rapirsîya te qediya",
"keyobard": {
"show_everything": "Her tiştî nîşan bide",
- "show_mentions": "Behskirîya nîşan bike"
+ "show_mentions": "Qalkirinan nîşan bike"
}
},
"thread": {
"back_title": "Şandî",
- "title": "Post from %s"
+ "title": "Şandî ji %s"
},
"settings": {
"title": "Sazkarî",
@@ -481,8 +488,8 @@
"notifications": {
"title": "Agahdarî",
"favorites": "Şandiyên min hez kir",
- "follows": "Min şopand",
- "boosts": "Reblogs my post",
+ "follows": "Min dişopîne",
+ "boosts": "Şandiya min ji nû ve nivîsand",
"mentions": "Qale min kir",
"trigger": {
"anyone": "her kes",
@@ -494,39 +501,39 @@
},
"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"
+ "true_black_dark_mode": "Moda tarî ya reş a rastîn",
+ "disable_avatar_animation": "Avatarên anîmasyonî neçalak bike",
+ "disable_emoji_animation": "Emojiyên anîmasyonî neçalak bike",
+ "using_default_browser": "Ji bo vekirina girêdanan geroka berdest bi kar bîne"
},
"boring_zone": {
- "title": "The Boring Zone",
- "account_settings": "Account Settings",
- "terms": "Terms of Service",
- "privacy": "Privacy Policy"
+ "title": "Devera acizker",
+ "account_settings": "Sazkariyên ajimêr",
+ "terms": "Mercên bikaranînê",
+ "privacy": "Polîtikaya nihêniyê"
},
"spicy_zone": {
- "title": "The Spicy Zone",
- "clear": "Clear Media Cache",
- "signout": "Sign Out"
+ "title": "Devera germ",
+ "clear": "Pêşbîra medyayê pak bike",
+ "signout": "Derkeve"
}
},
"footer": {
- "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
+ "mastodon_description": "Mastodon nermalava çavkaniya vekirî ye. Tu dikarî pirsgirêkan li ser GitHub-ê ragihînî di %s (%s) de"
},
"keyboard": {
- "close_settings_window": "Close Settings Window"
+ "close_settings_window": "Sazkariyên çarçoveyê bigire"
}
},
"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?",
+ "content1": "Şandiyên din hene ku tu dixwazî tevlî ragihandinê bikî?",
+ "content2": "Derbarê vê ragihandinê de tiştek heye ku divê çavdêr bizanin?",
"send": "Ragihandinê bişîne",
"skip_to_send": "Bêyî şirove bişîne",
- "text_placeholder": "Type or paste additional comments"
+ "text_placeholder": "Şiroveyên daxwazkirê binivîsine an jî pê ve bike"
},
"preview": {
"keyboard": {
@@ -537,13 +544,13 @@
},
"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",
+ "dismiss_account_switcher": "Guherkera ajimêrê paş guh bike",
"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"
+ "accessibility_hint": "Du caran bitikîne da ku çarçoveyahilpekok ji holê rakî"
}
}
}
\ No newline at end of file
diff --git a/Localization/StringsConvertor/input/ko_KR/app.json b/Localization/StringsConvertor/input/ko_KR/app.json
index a9fb71ee8..571b14659 100644
--- a/Localization/StringsConvertor/input/ko_KR/app.json
+++ b/Localization/StringsConvertor/input/ko_KR/app.json
@@ -67,6 +67,7 @@
"done": "완료",
"confirm": "확인",
"continue": "계속",
+ "compose": "Compose",
"cancel": "취소",
"discard": "버리기",
"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_bar": {
diff --git a/Localization/StringsConvertor/input/nl_NL/app.json b/Localization/StringsConvertor/input/nl_NL/app.json
index d57a38ef2..d8ee1e574 100644
--- a/Localization/StringsConvertor/input/nl_NL/app.json
+++ b/Localization/StringsConvertor/input/nl_NL/app.json
@@ -67,6 +67,7 @@
"done": "Klaar",
"confirm": "Bevestigen",
"continue": "Doorgaan",
+ "compose": "Compose",
"cancel": "Annuleren",
"discard": "Weggooien",
"try_again": "Probeer Opnieuw",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "Followers from other servers are not displayed."
+ },
+ "following": {
+ "footer": "Follows from other servers are not displayed."
+ },
"search": {
"title": "Zoeken",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/pt_BR/app.json b/Localization/StringsConvertor/input/pt_BR/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/pt_BR/app.json
+++ b/Localization/StringsConvertor/input/pt_BR/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/Localization/StringsConvertor/input/pt_PT/app.json b/Localization/StringsConvertor/input/pt_PT/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/pt_PT/app.json
+++ b/Localization/StringsConvertor/input/pt_PT/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/Localization/StringsConvertor/input/ro_RO/app.json b/Localization/StringsConvertor/input/ro_RO/app.json
index ef819f8e6..3927247ee 100644
--- a/Localization/StringsConvertor/input/ro_RO/app.json
+++ b/Localization/StringsConvertor/input/ro_RO/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/Localization/StringsConvertor/input/ru_RU/app.json b/Localization/StringsConvertor/input/ru_RU/app.json
index a5c34023a..c1ad3ee49 100644
--- a/Localization/StringsConvertor/input/ru_RU/app.json
+++ b/Localization/StringsConvertor/input/ru_RU/app.json
@@ -67,6 +67,7 @@
"done": "Готово",
"confirm": "Подтвердить",
"continue": "Продолжить",
+ "compose": "Compose",
"cancel": "Отмена",
"discard": "Отмена",
"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_bar": {
diff --git a/Localization/StringsConvertor/input/sv_FI/app.json b/Localization/StringsConvertor/input/sv_FI/app.json
index d0a9ad795..7acf48755 100644
--- a/Localization/StringsConvertor/input/sv_FI/app.json
+++ b/Localization/StringsConvertor/input/sv_FI/app.json
@@ -67,6 +67,7 @@
"done": "Done",
"confirm": "Confirm",
"continue": "Fortsätt",
+ "compose": "Compose",
"cancel": "Avbryt",
"discard": "Discard",
"try_again": "Försök igen",
@@ -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/Localization/StringsConvertor/input/sv_SE/app.json b/Localization/StringsConvertor/input/sv_SE/app.json
index d0a9ad795..7acf48755 100644
--- a/Localization/StringsConvertor/input/sv_SE/app.json
+++ b/Localization/StringsConvertor/input/sv_SE/app.json
@@ -67,6 +67,7 @@
"done": "Done",
"confirm": "Confirm",
"continue": "Fortsätt",
+ "compose": "Compose",
"cancel": "Avbryt",
"discard": "Discard",
"try_again": "Försök igen",
@@ -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/Localization/StringsConvertor/input/th_TH/app.json b/Localization/StringsConvertor/input/th_TH/app.json
index fb3024f2b..7852b5d01 100644
--- a/Localization/StringsConvertor/input/th_TH/app.json
+++ b/Localization/StringsConvertor/input/th_TH/app.json
@@ -67,6 +67,7 @@
"done": "เสร็จสิ้น",
"confirm": "ยืนยัน",
"continue": "ดำเนินการต่อ",
+ "compose": "เขียน",
"cancel": "ยกเลิก",
"discard": "ละทิ้ง",
"try_again": "ลองอีกครั้ง",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "ไม่ได้แสดงผู้ติดตามจากเซิร์ฟเวอร์อื่น ๆ"
+ },
+ "following": {
+ "footer": "ไม่ได้แสดงการติดตามจากเซิร์ฟเวอร์อื่น ๆ"
+ },
"search": {
"title": "ค้นหา",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/zh_CN/app.json b/Localization/StringsConvertor/input/zh_CN/app.json
index b7728b60c..905afdd86 100644
--- a/Localization/StringsConvertor/input/zh_CN/app.json
+++ b/Localization/StringsConvertor/input/zh_CN/app.json
@@ -67,6 +67,7 @@
"done": "完成",
"confirm": "确认",
"continue": "继续",
+ "compose": "撰写",
"cancel": "取消",
"discard": "放弃",
"try_again": "再试一次",
@@ -413,6 +414,12 @@
}
}
},
+ "follower": {
+ "footer": "不会显示来自其它服务器的关注者"
+ },
+ "following": {
+ "footer": "不会显示来自其它服务器的关注"
+ },
"search": {
"title": "搜索",
"search_bar": {
diff --git a/Localization/StringsConvertor/input/zh_TW/app.json b/Localization/StringsConvertor/input/zh_TW/app.json
index 3ec77cf10..5c01ae7e0 100644
--- a/Localization/StringsConvertor/input/zh_TW/app.json
+++ b/Localization/StringsConvertor/input/zh_TW/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/Localization/app.json b/Localization/app.json
index 5c01ae7e0..6d3b2fcc2 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -82,6 +82,7 @@
"share_user": "Share %s",
"share_post": "Share Post",
"open_in_safari": "Open in Safari",
+ "open_in_browser": "Open in Browser",
"find_people": "Find people to follow",
"manually_search": "Manually search instead",
"skip": "Skip",
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 60f0f5d74..6b7644e3e 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -296,6 +296,11 @@
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */; };
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */; };
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F11725EFA35B001F1DAB /* StripProgressView.swift */; };
+ DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B7294273112B100081888 /* FollowingListViewController.swift */; };
+ DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B7297273112C800081888 /* FollowingListViewModel.swift */; };
+ DB5B729A2731137900081888 /* FollowingListViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B72992731137900081888 /* FollowingListViewController+Provider.swift */; };
+ DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */; };
+ DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5B729D273113F300081888 /* FollowingListViewModel+State.swift */; };
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */; };
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180DF2639194B0018D199 /* MediaPreviewPagingViewController.swift */; };
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180E226391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift */; };
@@ -310,11 +315,13 @@
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */; };
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */; };
DB63BE7F268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */; };
- DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */; };
DB647C5926F1EA2700F7F82C /* WizardPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB647C5826F1EA2700F7F82C /* WizardPreference.swift */; };
DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; };
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
+ DB67D08427312970006A36CF /* APIService+Following.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08327312970006A36CF /* APIService+Following.swift */; };
+ DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D08527312E67006A36CF /* WizardViewController.swift */; };
+ DB67D089273256D7006A36CF /* StoreReviewPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB67D088273256D7006A36CF /* StoreReviewPreference.swift */; };
DB68045B2636DC6A00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; };
DB6804662636DC9000430867 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; };
DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68045A2636DC6A00430867 /* MastodonNotification.swift */; };
@@ -370,6 +377,7 @@
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */; };
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
+ DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.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 */; };
@@ -600,6 +608,8 @@
DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */; };
DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */; };
DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */; };
+ DBF3B73F2733EAED00E21627 /* local-codes.json in Resources */ = {isa = PBXBuildFile; fileRef = DBF3B73E2733EAED00E21627 /* local-codes.json */; };
+ DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */; };
DBF7A0FC26830C33004176A2 /* FPSIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = DBF7A0FB26830C33004176A2 /* FPSIndicator */; };
DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; };
DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -1110,6 +1120,11 @@
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellHeightCacheableContainer.swift; sourceTree = ""; };
DB59F10D25EF724F001F1DAB /* APIService+Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Poll.swift"; sourceTree = ""; };
DB59F11725EFA35B001F1DAB /* StripProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripProgressView.swift; sourceTree = ""; };
+ DB5B7294273112B100081888 /* FollowingListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingListViewController.swift; sourceTree = ""; };
+ DB5B7297273112C800081888 /* FollowingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingListViewModel.swift; sourceTree = ""; };
+ DB5B72992731137900081888 /* FollowingListViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewController+Provider.swift"; sourceTree = ""; };
+ DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+Diffable.swift"; sourceTree = ""; };
+ DB5B729D273113F300081888 /* FollowingListViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FollowingListViewModel+State.swift"; sourceTree = ""; };
DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewController.swift; sourceTree = ""; };
DB6180DF2639194B0018D199 /* MediaPreviewPagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewPagingViewController.swift; sourceTree = ""; };
DB6180E226391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerAnimatedTransitioning.swift; sourceTree = ""; };
@@ -1124,11 +1139,13 @@
DB6180F726391D660018D199 /* MediaPreviewingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewingViewController.swift; sourceTree = ""; };
DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewViewModel.swift; sourceTree = ""; };
DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationViewController+StatusProvider.swift"; sourceTree = ""; };
- DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainTabBarController+Wizard.swift"; sourceTree = ""; };
DB647C5826F1EA2700F7F82C /* WizardPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardPreference.swift; sourceTree = ""; };
DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = ""; };
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = ""; };
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = ""; };
+ DB67D08327312970006A36CF /* APIService+Following.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Following.swift"; sourceTree = ""; };
+ DB67D08527312E67006A36CF /* WizardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardViewController.swift; sourceTree = ""; };
+ DB67D088273256D7006A36CF /* StoreReviewPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReviewPreference.swift; sourceTree = ""; };
DB68045A2636DC6A00430867 /* MastodonNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonNotification.swift; sourceTree = ""; };
DB68047F2637CD4C00430867 /* AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB6804812637CD4C00430867 /* AppShared.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppShared.h; sourceTree = ""; };
@@ -1179,6 +1196,7 @@
DB71FD5125F8CCAA00512AE1 /* APIService+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Status.swift"; sourceTree = ""; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; };
+ DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.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 = ""; };
@@ -1412,6 +1430,8 @@
DBF1D24D269DAF5D00C1C08A /* SearchDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewController.swift; sourceTree = ""; };
DBF1D250269DB01200C1C08A /* SearchHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryViewController.swift; sourceTree = ""; };
DBF1D256269DBAC600C1C08A /* SearchDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDetailViewModel.swift; sourceTree = ""; };
+ DBF3B73E2733EAED00E21627 /* local-codes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "local-codes.json"; sourceTree = ""; };
+ DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLocalCode.swift; sourceTree = ""; };
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = ""; };
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = ""; };
DBF8AE13263293E400C9C23C /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -2198,6 +2218,7 @@
isa = PBXGroup;
children = (
164F0EBB267D4FE400249499 /* BoopSound.caf */,
+ DBF3B73E2733EAED00E21627 /* local-codes.json */,
DB427DDE25BAA00100D1B89D /* Assets.xcassets */,
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */,
DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */,
@@ -2316,6 +2337,7 @@
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
DB6B74F9272FC2B500C70B6E /* APIService+Follower.swift */,
+ DB67D08327312970006A36CF /* APIService+Following.swift */,
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
5B24BBE1262DB19100A9381B /* APIService+Report.swift */,
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
@@ -2434,6 +2456,7 @@
DB1D842F26566512000346B3 /* KeyboardPreference.swift */,
DBCBCC0C2680B908000F5B51 /* HomeTimelinePreference.swift */,
DBD376AB2692ECDB007FEC24 /* ThemePreference.swift */,
+ DB67D088273256D7006A36CF /* StoreReviewPreference.swift */,
);
path = Preference;
sourceTree = "";
@@ -2453,6 +2476,18 @@
path = View;
sourceTree = "";
};
+ DB5B7296273112B400081888 /* Following */ = {
+ isa = PBXGroup;
+ children = (
+ DB5B7294273112B100081888 /* FollowingListViewController.swift */,
+ DB5B72992731137900081888 /* FollowingListViewController+Provider.swift */,
+ DB5B7297273112C800081888 /* FollowingListViewModel.swift */,
+ DB5B729B273113C200081888 /* FollowingListViewModel+Diffable.swift */,
+ DB5B729D273113F300081888 /* FollowingListViewModel+State.swift */,
+ );
+ path = Following;
+ sourceTree = "";
+ };
DB6180DE263919350018D199 /* MediaPreview */ = {
isa = PBXGroup;
children = (
@@ -2504,6 +2539,14 @@
path = Image;
sourceTree = "";
};
+ DB67D08727312E6A006A36CF /* Wizard */ = {
+ isa = PBXGroup;
+ children = (
+ DB67D08527312E67006A36CF /* WizardViewController.swift */,
+ );
+ path = Wizard;
+ sourceTree = "";
+ };
DB6804802637CD4C00430867 /* AppShared */ = {
isa = PBXGroup;
children = (
@@ -2745,7 +2788,6 @@
isa = PBXGroup;
children = (
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */,
- DB647C5626F1E97300F7F82C /* MainTabBarController+Wizard.swift */,
);
path = MainTab;
sourceTree = "";
@@ -2757,6 +2799,7 @@
DB6180E426391A500018D199 /* Transition */,
DB852D1D26FB021900FC9D81 /* Root */,
DB01409B25C40BB600F9F3CF /* Onboarding */,
+ DB67D08727312E6A006A36CF /* Wizard */,
DB9F58ED26EF435800E7BBE9 /* Account */,
2D38F1D325CD463600561493 /* HomeTimeline */,
2D76316325C14BAC00929FB9 /* PublicTimeline */,
@@ -2902,6 +2945,7 @@
DBB5253B2611ECF5002F1F29 /* Timeline */,
DBE3CDF1261C6B3100430CC6 /* Favorite */,
DB6B74F0272FB55400C70B6E /* Follower */,
+ DB5B7296273112B400081888 /* Following */,
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */,
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
@@ -2930,6 +2974,7 @@
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */,
2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */,
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */,
+ DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */,
);
path = ViewModel;
sourceTree = "";
@@ -3109,6 +3154,7 @@
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
DBBC24D826A54BCB00398BB9 /* MastodonMetricFormatter.swift */,
DBFEF07626A691FB006D7ED1 /* MastodonAuthenticationBox.swift */,
+ DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
);
path = Helper;
sourceTree = "";
@@ -3626,6 +3672,7 @@
DB427DDF25BAA00100D1B89D /* Assets.xcassets in Resources */,
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */,
DBA4B0F626C269880077136E /* Intents.stringsdict in Resources */,
+ DBF3B73F2733EAED00E21627 /* local-codes.json in Resources */,
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */,
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */,
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */,
@@ -3918,6 +3965,7 @@
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
DB1D843426579931000346B3 /* TableViewControllerNavigateable.swift in Sources */,
+ DB5B729A2731137900081888 /* FollowingListViewController+Provider.swift in Sources */,
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
@@ -3929,6 +3977,7 @@
DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */,
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */,
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
+ DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
5DF1054125F886D400D6C0D4 /* VideoPlaybackService.swift in Sources */,
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */,
@@ -4065,6 +4114,8 @@
DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */,
2D82BA0525E7897700E36F0F /* MastodonResendEmailViewModelNavigationDelegateShim.swift in Sources */,
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
+ DB67D089273256D7006A36CF /* StoreReviewPreference.swift in Sources */,
+ DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */,
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */,
@@ -4075,8 +4126,10 @@
DB73BF47271199CA00781945 /* Instance.swift in Sources */,
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
+ DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */,
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
+ DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */,
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
@@ -4097,6 +4150,7 @@
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
DB73BF43271192BB00781945 /* InstanceService.swift in Sources */,
+ DB67D08427312970006A36CF /* APIService+Following.swift in Sources */,
DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */,
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
@@ -4182,6 +4236,7 @@
2D24E11D2626D8B100A59D4F /* NotificationStatusTableViewCell.swift in Sources */,
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
+ DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */,
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */,
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
@@ -4326,6 +4381,7 @@
DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
DBBC24C026A5443100398BB9 /* MastodonTheme.swift in Sources */,
+ DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
DBBC24B526A540AE00398BB9 /* AvatarConfigurableView.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
DBFEF07726A691FB006D7ED1 /* MastodonAuthenticationBox.swift in Sources */,
@@ -4339,7 +4395,6 @@
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
- DB647C5726F1E97300F7F82C /* MainTabBarController+Wizard.swift in Sources */,
DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */,
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
DB6B74F8272FBFB100C70B6E /* FollowerListViewController+Provider.swift in Sources */,
@@ -4356,6 +4411,7 @@
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */,
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
+ DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */,
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */,
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
@@ -4863,7 +4919,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4878,7 +4934,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -4892,7 +4948,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -4906,7 +4962,7 @@
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -5000,11 +5056,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5031,11 +5087,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5060,11 +5116,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = CoreDataStack/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5090,11 +5146,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = CoreDataStack/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5157,7 +5213,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5171,7 +5227,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -5182,7 +5238,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5196,7 +5252,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Debug";
@@ -5207,7 +5263,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5221,7 +5277,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Release";
@@ -5232,7 +5288,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5246,7 +5302,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -5257,7 +5313,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5271,7 +5327,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -5282,7 +5338,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5296,7 +5352,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Debug";
@@ -5307,7 +5363,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5321,7 +5377,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Release";
@@ -5332,7 +5388,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5346,7 +5402,7 @@
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -5423,7 +5479,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -5438,7 +5494,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Release";
@@ -5490,11 +5546,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = CoreDataStack/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5539,7 +5595,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5552,7 +5608,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Release";
@@ -5564,11 +5620,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5660,7 +5716,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist;
@@ -5675,7 +5731,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Mastodon/Vender/Mastodon-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Debug";
@@ -5727,11 +5783,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = CoreDataStack/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5776,7 +5832,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5789,7 +5845,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = "ASDK - Debug";
@@ -5801,11 +5857,11 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 82;
+ DYLIB_CURRENT_VERSION = 88;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -5831,7 +5887,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5844,7 +5900,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -5855,7 +5911,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 82;
+ CURRENT_PROJECT_VERSION = 88;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -5868,7 +5924,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
+ TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -6026,7 +6082,7 @@
repositoryURL = "https://github.com/TwidereProject/MetaTextKit.git";
requirement = {
kind = exactVersion;
- version = 2.1.1;
+ version = 2.1.2;
};
};
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index cb88c3960..5c99e944b 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,87 +7,92 @@
AppShared.xcscheme_^#shared#^_
orderHint
- 42
+ 44
CoreDataStack.xcscheme_^#shared#^_
orderHint
- 43
+ 45
Mastodon - ASDK.xcscheme_^#shared#^_
orderHint
- 3
+ 4
Mastodon - RTL.xcscheme_^#shared#^_
orderHint
- 13
+ 17
Mastodon - Release.xcscheme_^#shared#^_
orderHint
- 2
+ 3
Mastodon - ar.xcscheme_^#shared#^_
orderHint
- 10
+ 11
Mastodon - ca.xcscheme_^#shared#^_
orderHint
- 16
+ 23
Mastodon - de.xcscheme_^#shared#^_
orderHint
- 11
+ 13
- Mastodon - en.xcscheme_^#shared#^_
+ Mastodon - double length.xcscheme_^#shared#^_
orderHint
1
+ Mastodon - en.xcscheme_^#shared#^_
+
+ orderHint
+ 2
+
Mastodon - es-419.xcscheme_^#shared#^_
-
- orderHint
- 8
-
- Mastodon - es.xcscheme_^#shared#^_
-
- orderHint
- 7
-
- Mastodon - fr.xcscheme_^#shared#^_
orderHint
9
+ Mastodon - es.xcscheme_^#shared#^_
+
+ orderHint
+ 8
+
+ Mastodon - fr.xcscheme_^#shared#^_
+
+ orderHint
+ 10
+
Mastodon - jp.xcscheme_^#shared#^_
orderHint
- 14
+ 19
Mastodon - nl.xcscheme_^#shared#^_
orderHint
- 12
+ 15
Mastodon - ru.xcscheme_^#shared#^_
orderHint
- 4
+ 5
Mastodon - th.xcscheme_^#shared#^_
orderHint
- 5
+ 6
Mastodon - zh_Hans.xcscheme_^#shared#^_
orderHint
- 15
+ 21
Mastodon.xcscheme_^#shared#^_
@@ -97,7 +102,7 @@
MastodonIntent.xcscheme_^#shared#^_
orderHint
- 44
+ 43
MastodonIntents.xcscheme_^#shared#^_
@@ -112,12 +117,12 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 6
+ 7
ShareActionExtension.xcscheme_^#shared#^_
orderHint
- 41
+ 42
SuppressBuildableAutocreation
diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index b305c8156..11dde7269 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -96,8 +96,8 @@
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"state": {
"branch": null,
- "revision": "a5f412b72fc08cd1348e2388fc7ec326365e1823",
- "version": "2.1.1"
+ "revision": "7af4182f64329440a4656f2cba307cb5848e496a",
+ "version": "2.1.2"
}
},
{
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index cda20255b..9fbb2b774 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -23,6 +23,7 @@ final public class SceneCoordinator {
private(set) weak var tabBarController: MainTabBarController!
private(set) weak var splitViewController: RootSplitViewController?
+ private(set) var wizardViewController: WizardViewController?
private(set) var secondaryStackHashValues = Set()
@@ -179,6 +180,7 @@ extension SceneCoordinator {
case profile(viewModel: ProfileViewModel)
case favorite(viewModel: FavoriteViewModel)
case follower(viewModel: FollowerListViewModel)
+ case following(viewModel: FollowingListViewModel)
// setting
case settings(viewModel: SettingsViewModel)
@@ -220,17 +222,34 @@ extension SceneCoordinator {
extension SceneCoordinator {
func setup() {
+ let rootViewController: UIViewController
switch UIDevice.current.userInterfaceIdiom {
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self)
- sceneDelegate.window?.rootViewController = viewController
- tabBarController = viewController
+ self.splitViewController = nil
+ self.tabBarController = viewController
+ rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
- sceneDelegate.window?.rootViewController = splitViewController
+ rootViewController = splitViewController
}
+
+ let wizardViewController = WizardViewController()
+ if !wizardViewController.items.isEmpty,
+ let delegate = rootViewController as? WizardViewControllerDelegate
+ {
+ // do not add as child view controller.
+ // otherwise, the tab bar controller will add as a new tab
+ wizardViewController.delegate = delegate
+ wizardViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ wizardViewController.view.frame = rootViewController.view.bounds
+ rootViewController.view.addSubview(wizardViewController.view)
+ self.wizardViewController = wizardViewController
+ }
+
+ sceneDelegate.window?.rootViewController = rootViewController
}
func setupOnboardingIfNeeds(animated: Bool) {
@@ -429,6 +448,10 @@ private extension SceneCoordinator {
let _viewController = FollowerListViewController()
_viewController.viewModel = viewModel
viewController = _viewController
+ case .following(let viewModel):
+ let _viewController = FollowingListViewController()
+ _viewController.viewModel = viewModel
+ viewController = _viewController
case .suggestionAccount(let viewModel):
let _viewController = SuggestionAccountViewController()
_viewController.viewModel = viewModel
diff --git a/Mastodon/Diffiable/Item/NotificationItem.swift b/Mastodon/Diffiable/Item/NotificationItem.swift
index 22949b3a5..fc7d0e0d9 100644
--- a/Mastodon/Diffiable/Item/NotificationItem.swift
+++ b/Mastodon/Diffiable/Item/NotificationItem.swift
@@ -10,7 +10,7 @@ import Foundation
enum NotificationItem {
case notification(objectID: NSManagedObjectID, attribute: Item.StatusAttribute)
-
+ case notificationStatus(objectID: NSManagedObjectID, attribute: Item.StatusAttribute) // display notification status without card wrapper
case bottomLoader
}
@@ -19,6 +19,8 @@ extension NotificationItem: Equatable {
switch (lhs, rhs) {
case (.notification(let idLeft, _), .notification(let idRight, _)):
return idLeft == idRight
+ case (.notificationStatus(let idLeft, _), .notificationStatus(let idRight, _)):
+ return idLeft == idRight
case (.bottomLoader, .bottomLoader):
return true
default:
@@ -32,6 +34,8 @@ extension NotificationItem: Hashable {
switch self {
case .notification(let id, _):
hasher.combine(id)
+ case .notificationStatus(let id, _):
+ hasher.combine(id)
case .bottomLoader:
hasher.combine(String(describing: NotificationItem.bottomLoader.self))
}
@@ -43,6 +47,8 @@ extension NotificationItem {
switch self {
case .notification(let objectID, _):
return .mastodonNotification(objectID: objectID)
+ case .notificationStatus(let objectID, _):
+ return .mastodonNotification(objectID: objectID)
case .bottomLoader:
return nil
}
diff --git a/Mastodon/Diffiable/Item/UserItem.swift b/Mastodon/Diffiable/Item/UserItem.swift
index 6f3c591b1..bd15f35ea 100644
--- a/Mastodon/Diffiable/Item/UserItem.swift
+++ b/Mastodon/Diffiable/Item/UserItem.swift
@@ -10,6 +10,7 @@ import CoreData
enum UserItem: Hashable {
case follower(objectID: NSManagedObjectID)
+ case following(objectID: NSManagedObjectID)
case bottomLoader
case bottomHeader(text: String)
}
diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift
index 215ba67cb..864785007 100644
--- a/Mastodon/Diffiable/Section/Status/NotificationSection.swift
+++ b/Mastodon/Diffiable/Section/Status/NotificationSection.swift
@@ -21,9 +21,10 @@ enum NotificationSection: Equatable, Hashable {
extension NotificationSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
+ dependency: NeedsDependency,
managedObjectContext: NSManagedObjectContext,
delegate: NotificationTableViewCellDelegate,
- dependency: NeedsDependency
+ statusTableViewCellDelegate: StatusTableViewCellDelegate
) -> UITableViewDiffableDataSource {
UITableViewDiffableDataSource(tableView: tableView) {
[weak delegate, weak dependency]
@@ -32,137 +33,47 @@ extension NotificationSection {
switch notificationItem {
case .notification(let objectID, let attribute):
guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
- !notification.isDeleted else {
- return UITableViewCell()
- }
+ !notification.isDeleted
+ else { return UITableViewCell() }
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
- cell.delegate = delegate
-
- // configure author
- cell.configure(
- with: AvatarConfigurableViewConfiguration(
- avatarImageURL: notification.account.avatarImageURL()
- )
+ configure(
+ tableView: tableView,
+ cell: cell,
+ notification: notification,
+ dependency: dependency,
+ attribute: attribute
)
+ cell.delegate = delegate
+ cell.isAccessibilityElement = true
+ NotificationSection.configureStatusAccessibilityLabel(cell: cell)
+ return cell
- func createActionImage() -> UIImage? {
- return UIImage(
- systemName: notification.notificationType.actionImageName,
- withConfiguration: UIImage.SymbolConfiguration(
- pointSize: 12, weight: .semibold
- )
- )?
- .withTintColor(.systemBackground)
- .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
- }
+ case .notificationStatus(objectID: let objectID, attribute: let attribute):
+ guard let notification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification,
+ !notification.isDeleted,
+ let status = notification.status,
+ let requestUserID = dependency.context.authenticationService.activeMastodonAuthenticationBox.value?.userID
+ else { return UITableViewCell() }
+ let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: StatusTableViewCell.self), for: indexPath) as! StatusTableViewCell
- cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
- cell.avatarButton.badgeImageView.image = createActionImage()
- cell.traitCollectionDidChange
- .receive(on: DispatchQueue.main)
- .sink { [weak cell] in
- guard let cell = cell else { return }
- cell.avatarButton.badgeImageView.image = createActionImage()
- }
- .store(in: &cell.disposeBag)
-
- // configure author name, notification description, timestamp
- let nameText = notification.account.displayNameWithFallback
- let titleLabelText: String = {
- switch notification.notificationType {
- case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
- case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
- case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
- case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
- case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
- case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
- default: return ""
- }
- }()
-
- do {
- let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
- let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
-
- let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
- let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
-
- cell.titleLabel.configure(content: metaContent)
-
- if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
- let nsRange = NSRange(nameRange, in: metaContent.string)
- cell.titleLabel.textStorage.addAttributes([
- .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
- .foregroundColor: Asset.Colors.brandBlue.color,
- ], range: nsRange)
- }
-
- } catch {
- let metaContent = PlaintextMetaContent(string: titleLabelText)
- cell.titleLabel.configure(content: metaContent)
- }
-
- let createAt = notification.createAt
- cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
- AppContext.shared.timestampUpdatePublisher
- .receive(on: DispatchQueue.main)
- .sink { [weak cell] _ in
- guard let cell = cell else { return }
- cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
- }
- .store(in: &cell.disposeBag)
-
- // configure follow request (if exist)
- if case .followRequest = notification.notificationType {
- cell.acceptButton.publisher(for: .touchUpInside)
- .sink { [weak cell] _ in
- guard let cell = cell else { return }
- cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
- }
- .store(in: &cell.disposeBag)
- cell.rejectButton.publisher(for: .touchUpInside)
- .sink { [weak cell] _ in
- guard let cell = cell else { return }
- cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
- }
- .store(in: &cell.disposeBag)
- cell.buttonStackView.isHidden = false
- } else {
- cell.buttonStackView.isHidden = true
- }
-
- // configure status (if exist)
- if let status = notification.status {
- let frame = CGRect(
- x: 0,
- y: 0,
- width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right,
- height: tableView.readableContentGuide.layoutFrame.height
- )
- StatusSection.configure(
- cell: cell,
- tableView: tableView,
- timelineContext: .notifications,
- dependency: dependency,
- readableLayoutFrame: frame,
- status: status,
- requestUserID: notification.userID,
- statusItemAttribute: attribute
- )
- cell.statusContainerView.isHidden = false
- cell.containerStackView.alignment = .top
- cell.containerStackViewBottomLayoutConstraint.constant = 0
- } else {
- if case .followRequest = notification.notificationType {
- cell.containerStackView.alignment = .top
- } else {
- cell.containerStackView.alignment = .center
- }
- cell.statusContainerView.isHidden = true
- cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
- }
-
+ // configure cell
+ StatusSection.configureStatusTableViewCell(
+ cell: cell,
+ tableView: tableView,
+ timelineContext: .notifications,
+ dependency: dependency,
+ readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
+ status: status,
+ requestUserID: requestUserID,
+ statusItemAttribute: attribute
+ )
+ cell.statusView.headerContainerView.isHidden = true // set header hide
+ cell.statusView.actionToolbarContainer.isHidden = true // set toolbar hide
+ cell.statusView.actionToolbarPlaceholderPaddingView.isHidden = false
+ cell.delegate = statusTableViewCellDelegate
+ cell.isAccessibilityElement = true
+ StatusSection.configureStatusAccessibilityLabel(cell: cell)
return cell
case .bottomLoader:
@@ -174,3 +85,162 @@ extension NotificationSection {
}
}
+extension NotificationSection {
+ static func configure(
+ tableView: UITableView,
+ cell: NotificationStatusTableViewCell,
+ notification: MastodonNotification,
+ dependency: NeedsDependency,
+ attribute: Item.StatusAttribute
+ ) {
+ // configure author
+ cell.configure(
+ with: AvatarConfigurableViewConfiguration(
+ avatarImageURL: notification.account.avatarImageURL()
+ )
+ )
+
+ func createActionImage() -> UIImage? {
+ return UIImage(
+ systemName: notification.notificationType.actionImageName,
+ withConfiguration: UIImage.SymbolConfiguration(
+ pointSize: 12, weight: .semibold
+ )
+ )?
+ .withTintColor(.systemBackground)
+ .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14))
+ }
+
+ cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color
+ cell.avatarButton.badgeImageView.image = createActionImage()
+ cell.traitCollectionDidChange
+ .receive(on: DispatchQueue.main)
+ .sink { [weak cell] in
+ guard let cell = cell else { return }
+ cell.avatarButton.badgeImageView.image = createActionImage()
+ }
+ .store(in: &cell.disposeBag)
+
+ // configure author name, notification description, timestamp
+ let nameText = notification.account.displayNameWithFallback
+ let titleLabelText: String = {
+ switch notification.notificationType {
+ case .favourite: return L10n.Scene.Notification.userFavoritedYourPost(nameText)
+ case .follow: return L10n.Scene.Notification.userFollowedYou(nameText)
+ case .followRequest: return L10n.Scene.Notification.userRequestedToFollowYou(nameText)
+ case .mention: return L10n.Scene.Notification.userMentionedYou(nameText)
+ case .poll: return L10n.Scene.Notification.userYourPollHasEnded(nameText)
+ case .reblog: return L10n.Scene.Notification.userRebloggedYourPost(nameText)
+ default: return ""
+ }
+ }()
+
+ do {
+ let nameContent = MastodonContent(content: nameText, emojis: notification.account.emojiMeta)
+ let nameMetaContent = try MastodonMetaContent.convert(document: nameContent)
+
+ let mastodonContent = MastodonContent(content: titleLabelText, emojis: notification.account.emojiMeta)
+ let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
+
+ cell.titleLabel.configure(content: metaContent)
+
+ if let nameRange = metaContent.string.range(of: nameMetaContent.string) {
+ let nsRange = NSRange(nameRange, in: metaContent.string)
+ cell.titleLabel.textStorage.addAttributes([
+ .font: UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold), maximumPointSize: 20),
+ .foregroundColor: Asset.Colors.brandBlue.color,
+ ], range: nsRange)
+ }
+
+ } catch {
+ let metaContent = PlaintextMetaContent(string: titleLabelText)
+ cell.titleLabel.configure(content: metaContent)
+ }
+
+ let createAt = notification.createAt
+ cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
+ AppContext.shared.timestampUpdatePublisher
+ .receive(on: DispatchQueue.main)
+ .sink { [weak cell] _ in
+ guard let cell = cell else { return }
+ cell.timestampLabel.text = createAt.localizedSlowedTimeAgoSinceNow
+ }
+ .store(in: &cell.disposeBag)
+
+ // configure follow request (if exist)
+ if case .followRequest = notification.notificationType {
+ cell.acceptButton.publisher(for: .touchUpInside)
+ .sink { [weak cell] _ in
+ guard let cell = cell else { return }
+ cell.delegate?.notificationTableViewCell(cell, notification: notification, acceptButtonDidPressed: cell.acceptButton)
+ }
+ .store(in: &cell.disposeBag)
+ cell.rejectButton.publisher(for: .touchUpInside)
+ .sink { [weak cell] _ in
+ guard let cell = cell else { return }
+ cell.delegate?.notificationTableViewCell(cell, notification: notification, rejectButtonDidPressed: cell.rejectButton)
+ }
+ .store(in: &cell.disposeBag)
+ cell.buttonStackView.isHidden = false
+ } else {
+ cell.buttonStackView.isHidden = true
+ }
+
+ // configure status (if exist)
+ if let status = notification.status {
+ let frame = CGRect(
+ x: 0,
+ y: 0,
+ width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right,
+ height: tableView.readableContentGuide.layoutFrame.height
+ )
+ StatusSection.configure(
+ cell: cell,
+ tableView: tableView,
+ timelineContext: .notifications,
+ dependency: dependency,
+ readableLayoutFrame: frame,
+ status: status,
+ requestUserID: notification.userID,
+ statusItemAttribute: attribute
+ )
+ cell.statusContainerView.isHidden = false
+ cell.containerStackView.alignment = .top
+ cell.containerStackViewBottomLayoutConstraint.constant = 0
+ } else {
+ if case .followRequest = notification.notificationType {
+ cell.containerStackView.alignment = .top
+ } else {
+ cell.containerStackView.alignment = .center
+ }
+ cell.statusContainerView.isHidden = true
+ cell.containerStackViewBottomLayoutConstraint.constant = 5 // 5pt margin when no status view
+ }
+ }
+
+ static func configureStatusAccessibilityLabel(cell: NotificationStatusTableViewCell) {
+ // FIXME:
+ cell.accessibilityLabel = {
+ var accessibilityViews: [UIView?] = []
+ accessibilityViews.append(contentsOf: [
+ cell.titleLabel,
+ cell.timestampLabel,
+ cell.statusView
+ ])
+ if !cell.statusContainerView.isHidden {
+ if !cell.statusView.headerContainerView.isHidden {
+ accessibilityViews.append(cell.statusView.headerInfoLabel)
+ }
+ accessibilityViews.append(contentsOf: [
+ cell.statusView.nameMetaLabel,
+ cell.statusView.dateLabel,
+ cell.statusView.contentMetaText.textView,
+ ])
+ }
+ return accessibilityViews
+ .compactMap { $0?.accessibilityLabel }
+ .joined(separator: " ")
+ }()
+ }
+}
+
diff --git a/Mastodon/Diffiable/Section/Status/PollSection.swift b/Mastodon/Diffiable/Section/Status/PollSection.swift
index add2a79b2..682a2abc0 100644
--- a/Mastodon/Diffiable/Section/Status/PollSection.swift
+++ b/Mastodon/Diffiable/Section/Status/PollSection.swift
@@ -37,6 +37,15 @@ extension PollSection {
managedObjectContext.performAndWait {
let option = managedObjectContext.object(with: objectID) as! PollOption
PollSection.configure(cell: cell, pollOption: option, pollItemAttribute: attribute)
+
+ cell.isAccessibilityElement = true
+ cell.accessibilityLabel = {
+ var labels: [String] = [option.title]
+ if let percentage = cell.pollOptionView.optionPercentageLabel.text {
+ labels.append(percentage)
+ }
+ return labels.joined(separator: ",")
+ }()
}
return cell
}
diff --git a/Mastodon/Diffiable/Section/Status/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift
index ceb0c9458..61217c790 100644
--- a/Mastodon/Diffiable/Section/Status/StatusSection.swift
+++ b/Mastodon/Diffiable/Section/Status/StatusSection.swift
@@ -158,6 +158,11 @@ extension StatusSection {
accessibilityElements.append(cell.statusView.avatarView)
accessibilityElements.append(cell.statusView.nameMetaLabel)
accessibilityElements.append(cell.statusView.dateLabel)
+ // poll
+ accessibilityElements.append(cell.statusView.pollTableView)
+ accessibilityElements.append(cell.statusView.pollVoteCountLabel)
+ accessibilityElements.append(cell.statusView.pollCountdownLabel)
+ accessibilityElements.append(cell.statusView.pollVoteButton)
// TODO: a11y
accessibilityElements.append(cell.statusView.contentMetaText.textView)
accessibilityElements.append(contentsOf: cell.statusView.statusMosaicImageViewContainer.imageViews)
@@ -389,7 +394,7 @@ extension StatusSection {
// set timestamp
let createdAt = (status.reblog ?? status).createdAt
cell.statusView.dateLabel.text = createdAt.localizedSlowedTimeAgoSinceNow
- cell.statusView.dateLabel.accessibilityValue = createdAt.timeAgoSinceNow
+ cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow
AppContext.shared.timestampUpdatePublisher
.receive(on: RunLoop.main) // will be paused when scrolling (on purpose)
.sink { [weak cell] _ in
@@ -978,6 +983,7 @@ extension StatusSection {
cell.statusView.pollCountdownLabel.text = "-"
}
+ cell.statusView.isUserInteractionEnabled = !poll.expired // make voice over touch passthroughable
cell.statusView.pollTableView.allowsSelection = !poll.expired
let votedOptions = poll.options.filter { option in
diff --git a/Mastodon/Diffiable/Section/UserSection.swift b/Mastodon/Diffiable/Section/UserSection.swift
index 58e80c6e3..9c7e2f212 100644
--- a/Mastodon/Diffiable/Section/UserSection.swift
+++ b/Mastodon/Diffiable/Section/UserSection.swift
@@ -30,7 +30,8 @@ extension UserSection {
] tableView, indexPath, item -> UITableViewCell? in
guard let dependency = dependency else { return UITableViewCell() }
switch item {
- case .follower(let objectID):
+ case .follower(let objectID),
+ .following(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
diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift
index 93cc4ca38..ebf9869c4 100644
--- a/Mastodon/Generated/Strings.swift
+++ b/Mastodon/Generated/Strings.swift
@@ -106,6 +106,8 @@ internal enum L10n {
}
/// Cancel
internal static let cancel = L10n.tr("Localizable", "Common.Controls.Actions.Cancel")
+ /// Compose
+ internal static let compose = L10n.tr("Localizable", "Common.Controls.Actions.Compose")
/// Confirm
internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm")
/// Continue
@@ -523,6 +525,14 @@ internal enum L10n {
/// Your Favorites
internal static let title = L10n.tr("Localizable", "Scene.Favorite.Title")
}
+ internal enum Follower {
+ /// Followers from other servers are not displayed.
+ internal static let footer = L10n.tr("Localizable", "Scene.Follower.Footer")
+ }
+ internal enum Following {
+ /// Follows from other servers are not displayed.
+ internal static let footer = L10n.tr("Localizable", "Scene.Following.Footer")
+ }
internal enum HomeTimeline {
/// Home
internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title")
diff --git a/Mastodon/Helper/MastodonLocalCode.swift b/Mastodon/Helper/MastodonLocalCode.swift
new file mode 100644
index 000000000..65d71895e
--- /dev/null
+++ b/Mastodon/Helper/MastodonLocalCode.swift
@@ -0,0 +1,12 @@
+//
+// MastodonLocalCode.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-4.
+//
+
+import Foundation
+
+// https://github.com/gunchleoc/mastodon/blob/ed6153b8f24d3a8f5a124cc95683bd1f20aec882/app/helpers/settings_helper.rb
+// last update 2021/11/4
+typealias MastodonLocalCode = [String: String]
diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist
index a75982e39..affa5b059 100644
--- a/Mastodon/Info.plist
+++ b/Mastodon/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 82
+ 88
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/Mastodon/Preference/StoreReviewPreference.swift b/Mastodon/Preference/StoreReviewPreference.swift
new file mode 100644
index 000000000..e3a403f6d
--- /dev/null
+++ b/Mastodon/Preference/StoreReviewPreference.swift
@@ -0,0 +1,26 @@
+//
+// StoreReviewPreference.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-3.
+//
+
+import Foundation
+
+extension UserDefaults {
+
+ @objc dynamic var processCompletedCount: Int {
+ get {
+ return integer(forKey: #function)
+ }
+ set { self[#function] = newValue }
+ }
+
+ @objc dynamic var lastVersionPromptedForReview: String? {
+ get {
+ return string(forKey: #function)
+ }
+ set { self[#function] = newValue }
+ }
+
+}
diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings
index 5950546a9..b878e0342 100644
--- a/Mastodon/Resources/ar.lproj/Localizable.strings
+++ b/Mastodon/Resources/ar.lproj/Localizable.strings
@@ -1,62 +1,63 @@
-"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.BlockDomain.BlockEntireDomain" = "حظر النِطاق";
+"Common.Alerts.BlockDomain.Title" = "هل أنتَ مُتأكِّدٌ حقًا مِن رغبتك في حظر %@ بالكامل؟ في معظم الحالات، يكون مِنَ الكافي والمُفَضَّل استهداف عدد محدود للحظر أو الكتم. لن ترى محتوى من هذا النطاق وسوف يتم إزالة جميع متابعيك المتواجدين فيه.";
"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.DeletePost.Title" = "هل أنت متأكد من رغبتك في حذف هذا المنشور؟";
+"Common.Alerts.DiscardPostContent.Message" = "أكِّد للتخلص مِن مُحتوى مَنشور مؤلَّف.";
+"Common.Alerts.DiscardPostContent.Title" = "التخلص من المسودة";
"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.
-Please check your internet connection.";
-"Common.Alerts.PublishPostFailure.Title" = "أخفقت عملية النشر";
-"Common.Alerts.SavePhotoFailure.Message" = "Please enable the photo library access permission to save the photo.";
-"Common.Alerts.SavePhotoFailure.Title" = "فشل حفظ الصورة";
+"Common.Alerts.PublishPostFailure.AttachmentsMessage.MoreThanOneVideo" = "لا يُمكِنُ إرفاق أكثر مِن مَقطع مرئي واحِد.";
+"Common.Alerts.PublishPostFailure.AttachmentsMessage.VideoAttachWithPhoto" = "لا يُمكن إرفاق مقطع مرئي إلى مَنشور يحتوي بالفعل على صُوَر.";
+"Common.Alerts.PublishPostFailure.Message" = "فَشَلَ نَشر المَنشور.
+يُرجى التحقق من اتصالك بالإنترنت.";
+"Common.Alerts.PublishPostFailure.Title" = "إخفاق في عمليَّة النشر";
+"Common.Alerts.SavePhotoFailure.Message" = "يُرجى إتاحة إذن الوصول إلى مكتبة الصور لحفظ الصورة.";
+"Common.Alerts.SavePhotoFailure.Title" = "إخفاق في حفظ الصورة";
"Common.Alerts.ServerError.Title" = "خطأ في الخادم";
"Common.Alerts.SignOut.Confirm" = "تسجيل الخروج";
-"Common.Alerts.SignOut.Message" = "هل أنت متأكد من أنك تريد تسجيل الخروج؟";
+"Common.Alerts.SignOut.Message" = "هل أنت متأكد من رغبتك في تسجيل الخروج؟";
"Common.Alerts.SignOut.Title" = "تسجيل الخروج";
-"Common.Alerts.SignUpFailure.Title" = "فشل التسجيل";
+"Common.Alerts.SignUpFailure.Title" = "إخفاق في التسجيل";
"Common.Alerts.VoteFailure.PollEnded" = "انتهى استطلاع الرأي";
-"Common.Alerts.VoteFailure.Title" = "فشل التصويت";
+"Common.Alerts.VoteFailure.Title" = "إخفاق في التصويت";
"Common.Controls.Actions.Add" = "إضافة";
"Common.Controls.Actions.Back" = "العودة";
"Common.Controls.Actions.BlockDomain" = "حظر %@";
"Common.Controls.Actions.Cancel" = "إلغاء";
+"Common.Controls.Actions.Compose" = "تأليف";
"Common.Controls.Actions.Confirm" = "تأكيد";
"Common.Controls.Actions.Continue" = "واصل";
"Common.Controls.Actions.CopyPhoto" = "نسخ الصورة";
"Common.Controls.Actions.Delete" = "احذف";
"Common.Controls.Actions.Discard" = "تجاهل";
"Common.Controls.Actions.Done" = "تمّ";
-"Common.Controls.Actions.Edit" = "تعديل";
+"Common.Controls.Actions.Edit" = "تحرير";
"Common.Controls.Actions.FindPeople" = "ابحث عن أشخاص لمتابعتهم";
"Common.Controls.Actions.ManuallySearch" = "البحث يدوياً بدلاً من ذلك";
"Common.Controls.Actions.Next" = "التالي";
"Common.Controls.Actions.Ok" = "حسنًا";
"Common.Controls.Actions.Open" = "افتح";
-"Common.Controls.Actions.OpenInSafari" = "افتحه في سفاري";
-"Common.Controls.Actions.Preview" = "معاينة";
+"Common.Controls.Actions.OpenInSafari" = "الفتح في Safari";
+"Common.Controls.Actions.Preview" = "مُعاينة";
"Common.Controls.Actions.Previous" = "السابق";
"Common.Controls.Actions.Remove" = "احذف";
-"Common.Controls.Actions.Reply" = "رد";
+"Common.Controls.Actions.Reply" = "الرَد";
"Common.Controls.Actions.ReportUser" = "ابلغ عن %@";
"Common.Controls.Actions.Save" = "حفظ";
"Common.Controls.Actions.SavePhoto" = "حفظ الصورة";
"Common.Controls.Actions.SeeMore" = "عرض المزيد";
"Common.Controls.Actions.Settings" = "الإعدادات";
-"Common.Controls.Actions.Share" = "شارك";
-"Common.Controls.Actions.SharePost" = "شارك المنشور";
-"Common.Controls.Actions.ShareUser" = "شارك %@";
+"Common.Controls.Actions.Share" = "المُشارك";
+"Common.Controls.Actions.SharePost" = "مشارك المنشور";
+"Common.Controls.Actions.ShareUser" = "مُشاركة %@";
"Common.Controls.Actions.SignIn" = "تسجيل الدخول";
"Common.Controls.Actions.SignUp" = "إنشاء حِساب";
"Common.Controls.Actions.Skip" = "تخطي";
-"Common.Controls.Actions.TakePhoto" = "التقط صورة";
+"Common.Controls.Actions.TakePhoto" = "التقاط صورة";
"Common.Controls.Actions.TryAgain" = "المُحاولة مرة أُخرى";
"Common.Controls.Actions.UnblockDomain" = "إلغاء حظر %@";
"Common.Controls.Friendship.Block" = "حظر";
@@ -75,7 +76,7 @@ Please check your internet connection.";
"Common.Controls.Friendship.UnblockUser" = "إلغاء حظر %@";
"Common.Controls.Friendship.Unmute" = "إلغاء الكتم";
"Common.Controls.Friendship.UnmuteUser" = "إلغاء كتم %@";
-"Common.Controls.Keyboard.Common.ComposeNewPost" = "إنشاء منشور جديد";
+"Common.Controls.Keyboard.Common.ComposeNewPost" = "تأليف منشور جديد";
"Common.Controls.Keyboard.Common.OpenSettings" = "أفتح الإعدادات";
"Common.Controls.Keyboard.Common.ShowFavorites" = "إظهار المفضلة";
"Common.Controls.Keyboard.Common.SwitchToTab" = "التبديل إلى %@";
@@ -88,9 +89,9 @@ Please check your internet connection.";
"Common.Controls.Keyboard.Timeline.PreviewImage" = "معاينة الصورة";
"Common.Controls.Keyboard.Timeline.PreviousStatus" = "المنشور السابق";
"Common.Controls.Keyboard.Timeline.ReplyStatus" = "رد على المنشور";
-"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Toggle Content Warning";
-"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Toggle Favorite on Post";
-"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Toggle Reblog on Post";
+"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "تبديل تحذير المُحتَوى";
+"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "تبديل المفضلة لِمنشور";
+"Common.Controls.Keyboard.Timeline.ToggleReblog" = "تبديل إعادة تدوين منشور";
"Common.Controls.Status.Actions.Favorite" = "إضافة إلى المفضلة";
"Common.Controls.Status.Actions.Menu" = "القائمة";
"Common.Controls.Status.Actions.Reblog" = "إعادة النشر";
@@ -98,9 +99,9 @@ Please check your internet connection.";
"Common.Controls.Status.Actions.Unfavorite" = "إزالة من المفضلة";
"Common.Controls.Status.Actions.Unreblog" = "تراجع عن إعادة النشر";
"Common.Controls.Status.ContentWarning" = "تحذير عن المحتوى";
-"Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
+"Common.Controls.Status.MediaContentWarning" = "انقر على أي مكان للكشف";
"Common.Controls.Status.Poll.Closed" = "انتهى";
-"Common.Controls.Status.Poll.Vote" = "صَوّت";
+"Common.Controls.Status.Poll.Vote" = "صَوِّت";
"Common.Controls.Status.ShowPost" = "اظهر المنشور";
"Common.Controls.Status.ShowUserProfile" = "اظهر الملف التعريفي للمستخدم";
"Common.Controls.Status.Tag.Email" = "البريد الإلكتروني";
@@ -116,43 +117,42 @@ Please check your internet connection.";
"Common.Controls.Tabs.Profile" = "الملف التعريفي";
"Common.Controls.Tabs.Search" = "بحث";
"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
-until you unblock them.
-Your profile looks like this to them.";
+"Common.Controls.Timeline.Header.BlockedWarning" = "لا يُمكِنُكَ عَرض الملف الشخصي لهذا المُستخدِم
+حتَّى يَرفَعَ الحَظر عَنك.";
+"Common.Controls.Timeline.Header.BlockingWarning" = "لا يُمكنك الاطلاع على الملف الشخصي لهذا المُستخدِم
+حتَّى تَرفعَ الحَظر عنه.
+ملفًّكَ الشخصي يَظهَرُ بِمثل هذِهِ الحالة بالنسبةِ لَهُ أيضًا.";
"Common.Controls.Timeline.Header.NoStatusFound" = "لا توجد هناك منشورات";
-"Common.Controls.Timeline.Header.SuspendedWarning" = "This user has been suspended.";
-"Common.Controls.Timeline.Header.UserBlockedWarning" = "You can’t view %@’s profile
-until they unblock you.";
-"Common.Controls.Timeline.Header.UserBlockingWarning" = "You can’t view %@’s profile
-until you unblock them.
-Your profile looks like this to them.";
-"Common.Controls.Timeline.Header.UserSuspendedWarning" = "%@’s account has been suspended.";
+"Common.Controls.Timeline.Header.SuspendedWarning" = "تمَّ إيقاف هذا المُستخدِم.";
+"Common.Controls.Timeline.Header.UserBlockedWarning" = "لا يُمكِنُكَ عَرض ملف %@ الشخصي
+حتَّى يَرفَعَ الحَظر عَنك.";
+"Common.Controls.Timeline.Header.UserBlockingWarning" = "لا يُمكنك الاطلاع على ملف %@ الشخصي
+حتَّى تَرفعَ الحَظر عنه.
+ملفًّكَ الشخصي يَظهَرُ بِمثل هذِهِ الحالة بالنسبةِ لَهُ أيضًا.";
+"Common.Controls.Timeline.Header.UserSuspendedWarning" = "لقد أوقِفَ حِساب %@.";
"Common.Controls.Timeline.Loader.LoadMissingPosts" = "تحميل المنشورات المَفقودة";
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "تحميل المزيد من المنشورات...";
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "إظهار المزيد من الردود";
"Common.Controls.Timeline.Timestamp.Now" = "الأن";
"Scene.AccountList.AddAccount" = "إضافة حساب";
"Scene.AccountList.DismissAccountSwitcher" = "تجاهُل مبدِّل الحساب";
-"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher";
+"Scene.AccountList.TabBarHint" = "المِلف المُحدَّد حاليًا: %@. انقر نقرًا مزدوجًا ثم اضغط مع الاستمرار لإظهار مُبدِّل الحِساب";
"Scene.Compose.Accessibility.AppendAttachment" = "إضافة مُرفَق";
"Scene.Compose.Accessibility.AppendPoll" = "اضافة استطلاع رأي";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي مخصص للإيموجي";
"Scene.Compose.Accessibility.DisableContentWarning" = "تعطيل تحذير الحتوى";
"Scene.Compose.Accessibility.EnableContentWarning" = "تنشيط تحذير المحتوى";
-"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post Visibility Menu";
+"Scene.Compose.Accessibility.PostVisibilityMenu" = "قائمة ظهور المنشور";
"Scene.Compose.Accessibility.RemovePoll" = "إزالة الاستطلاع";
-"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
-uploaded to 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.AttachmentBroken" = "هذا ال%@ مُعطَّل ويتعذَّر رفعه إلى ماستودون.";
+"Scene.Compose.Attachment.DescriptionPhoto" = "صِف الصورة للمكفوفين...";
+"Scene.Compose.Attachment.DescriptionVideo" = "صِف المقطع المرئي للمكفوفين...";
"Scene.Compose.Attachment.Photo" = "صورة";
"Scene.Compose.Attachment.Video" = "فيديو";
-"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
+"Scene.Compose.AutoComplete.SpaceToAdd" = "انقر مساحة لإضافتِها";
"Scene.Compose.ComposeAction" = "انشر";
"Scene.Compose.ContentInputPlaceholder" = "أخبِرنا بِما يَجُولُ فِي ذِهنَك";
-"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
+"Scene.Compose.ContentWarning.Placeholder" = "اكتب تَحذيرًا دَقيقًا هُنا...";
"Scene.Compose.Keyboard.AppendAttachmentEntry" = "إضافة مُرفَق - %@";
"Scene.Compose.Keyboard.DiscardPost" = "تجاهُل المنشور";
"Scene.Compose.Keyboard.PublishPost" = "نَشر المَنشُور";
@@ -179,10 +179,10 @@ uploaded to Mastodon.";
"Scene.Compose.Visibility.Unlisted" = "غير مُدرَج";
"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.Description" = "تحقق ممَّ إذا كان عنوان بريدك الإلكتروني صحيحًا وكذلك تأكد مِن مجلد البريد غير الهام إذا لم تكن قد فعلت ذلك.";
"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.Description" = "لقد أرسلنا لك بريدًا إلكترونيًا للتو. تحقق من مجلد البريد غير الهام الخاص بك إذا لم تكن قد فعلت ذلك.";
"Scene.ConfirmEmail.OpenEmailApp.Mail" = "البريد";
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "فتح عميل البريد الإلكتروني";
"Scene.ConfirmEmail.OpenEmailApp.Title" = "تحقَّق من بريدك الوارِد.";
@@ -190,6 +190,8 @@ uploaded to Mastodon.";
اضغط على الرابط لتأكيد حسابك.";
"Scene.ConfirmEmail.Title" = "شيء واحد أخير.";
"Scene.Favorite.Title" = "مفضلتك";
+"Scene.Follower.Footer" = "لا يُمكِن عَرض المُتابِعين مِنَ الخوادم الأُخرى.";
+"Scene.Following.Footer" = "لا يُمكِن عَرض المُتابَعات مِنَ الخوادم الأُخرى.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "إظهار منشورات جديدة";
"Scene.HomeTimeline.NavigationBarState.Offline" = "غير متصل";
"Scene.HomeTimeline.NavigationBarState.Published" = "تم نشره!";
@@ -205,7 +207,7 @@ uploaded to Mastodon.";
"Scene.Notification.UserRebloggedYourPost" = "أعاد %@ تدوين مشاركتك";
"Scene.Notification.UserRequestedToFollowYou" = "طلب %@ متابعتك";
"Scene.Notification.UserYourPollHasEnded" = "%@ اِنتهى استطلاعُكَ للرأي";
-"Scene.Preview.Keyboard.ClosePreview" = "إغلاق المعاينة";
+"Scene.Preview.Keyboard.ClosePreview" = "إغلاق المُعايَنَة";
"Scene.Preview.Keyboard.ShowNext" = "إظهار التالي";
"Scene.Preview.Keyboard.ShowPrevious" = "إظهار السابق";
"Scene.Profile.Dashboard.Followers" = "متابِع";
@@ -214,9 +216,9 @@ uploaded to Mastodon.";
"Scene.Profile.Fields.AddRow" = "إضافة صف";
"Scene.Profile.Fields.Placeholder.Content" = "المحتوى";
"Scene.Profile.Fields.Placeholder.Label" = "التسمية";
-"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm to unblock %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "أكِّد لرفع حظر %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "إلغاء حظر الحساب";
-"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "أكِّد لرفع كتمْ %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "إلغاء كتم الحساب";
"Scene.Profile.SegmentedControl.Media" = "وسائط";
"Scene.Profile.SegmentedControl.Posts" = "منشورات";
@@ -227,20 +229,20 @@ uploaded to Mastodon.";
"Scene.Register.Error.Item.Password" = "الكلمة السرية";
"Scene.Register.Error.Item.Reason" = "السبب";
"Scene.Register.Error.Item.Username" = "اسم المستخدم";
-"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted";
+"Scene.Register.Error.Reason.Accepted" = "يجب أن يُقبل %@";
"Scene.Register.Error.Reason.Blank" = "%@ مطلوب";
-"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed email provider";
-"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value";
+"Scene.Register.Error.Reason.Blocked" = "يحتوي %@ على موفِّر خدمة بريد إلكتروني غير مسموح به";
+"Scene.Register.Error.Reason.Inclusion" = "إنَّ %@ قيمة غير مدعومة";
"Scene.Register.Error.Reason.Invalid" = "%@ غير صالح";
-"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword";
-"Scene.Register.Error.Reason.Taken" = "%@ is already in use";
+"Scene.Register.Error.Reason.Reserved" = "إنَّ %@ عبارة عن كلمة مفتاحيَّة محجوزة";
+"Scene.Register.Error.Reason.Taken" = "إنَّ %@ مُستخدَمٌ بالفعل";
"Scene.Register.Error.Reason.TooLong" = "%@ طويل جداً";
"Scene.Register.Error.Reason.TooShort" = "%@ قصير جدا";
-"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist";
+"Scene.Register.Error.Reason.Unreachable" = "يبدوا أنَّ %@ غير موجود";
"Scene.Register.Error.Special.EmailInvalid" = "هذا عنوان بريد إلكتروني غير صالح";
"Scene.Register.Error.Special.PasswordTooShort" = "كلمة المرور قصيرة جداً (يجب أن تكون 8 أحرف على الأقل)";
-"Scene.Register.Error.Special.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores";
-"Scene.Register.Error.Special.UsernameTooLong" = "Username is too long (can’t be longer than 30 characters)";
+"Scene.Register.Error.Special.UsernameInvalid" = "يُمكِن أن يحتوي اسم المستخدم على أحرف أبجدية، أرقام وشرطات سفلية فقط";
+"Scene.Register.Error.Special.UsernameTooLong" = "اسم المستخدم طويل جداً (يجب ألّا يكون أطول من 30 رمز)";
"Scene.Register.Input.Avatar.Delete" = "احذف";
"Scene.Register.Input.DisplayName.Placeholder" = "الاسم المعروض";
"Scene.Register.Input.Email.Placeholder" = "البريد الإلكتروني";
@@ -250,24 +252,24 @@ uploaded to Mastodon.";
"Scene.Register.Input.Username.DuplicatePrompt" = "اسم المستخدم هذا غير متوفر.";
"Scene.Register.Input.Username.Placeholder" = "اسم المستخدم";
"Scene.Register.Title" = "أخبرنا عنك.";
-"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" = "ارسل الشكوى";
+"Scene.Report.Content1" = "هل ترغب في إضافة أي مشاركات أُخرى إلى الشكوى؟";
+"Scene.Report.Content2" = "هل هناك أي شيء يجب أن يعرفه المُراقبين حول هذه الشكوى؟";
+"Scene.Report.Send" = "إرسال الشكوى";
"Scene.Report.SkipToSend" = "إرسال بدون تعليق";
"Scene.Report.Step1" = "الخطوة 1 من 2";
"Scene.Report.Step2" = "الخطوة 2 من 2";
-"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
+"Scene.Report.TextPlaceholder" = "اكتب أو الصق تعليقات إضافيَّة";
"Scene.Report.Title" = "ابلغ عن %@";
"Scene.Search.Recommend.Accounts.Description" = "قد ترغب في متابعة هذه الحسابات";
"Scene.Search.Recommend.Accounts.Follow" = "تابع";
"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.Description" = "الوسوم التي تحظى بقدر كبير من الاهتمام";
"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ أشخاص يتحدَّثوا";
"Scene.Search.Recommend.HashTag.Title" = "ذات شعبية على ماستدون";
"Scene.Search.SearchBar.Cancel" = "إلغاء";
"Scene.Search.SearchBar.Placeholder" = "البحث عن وسوم أو مستخدمين·ات";
-"Scene.Search.Searching.Clear" = "امسح";
+"Scene.Search.Searching.Clear" = "مَحو";
"Scene.Search.Searching.EmptyState.NoResults" = "ليس هناك أية نتيجة";
"Scene.Search.Searching.RecentSearch" = "عمليات البحث الأخيرة";
"Scene.Search.Searching.Segment.All" = "الكل";
@@ -291,7 +293,7 @@ uploaded to Mastodon.";
"Scene.ServerPicker.Button.Category.Tech" = "تكنولوجيا";
"Scene.ServerPicker.Button.SeeLess" = "اعرض أقل";
"Scene.ServerPicker.Button.SeeMore" = "اعرض المزيد";
-"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading the data. Check your internet connection.";
+"Scene.ServerPicker.EmptyState.BadNetwork" = "حدث خطأٌ ما أثناء تحميل البيانات. تحقَّق من اتصالك بالإنترنت.";
"Scene.ServerPicker.EmptyState.FindingServers" = "البحث عن خوادم متوفرة...";
"Scene.ServerPicker.EmptyState.NoResults" = "لا توجد نتائج";
"Scene.ServerPicker.Input.Placeholder" = "ابحث عن خادم أو انضم إلى سيرفر خاص بك...";
@@ -306,7 +308,7 @@ uploaded to Mastodon.";
"Scene.ServerRules.Subtitle" = "تم سنّ هذه القواعد من قبل مشرفي %@.";
"Scene.ServerRules.TermsOfService" = "شروط الخدمة";
"Scene.ServerRules.Title" = "بعض القواعد الأساسية.";
-"Scene.Settings.Footer.MastodonDescription" = "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء، على غيت هب %@ (%@)";
+"Scene.Settings.Footer.MastodonDescription" = "ماستدون برنامج مفتوح المصدر. يمكنك المساهمة، أو الإبلاغ عن تقارير الأخطاء على GitHub في %@ (%@)";
"Scene.Settings.Keyboard.CloseSettingsWindow" = "إغلاق نافذة الإعدادات";
"Scene.Settings.Section.Appearance.Automatic" = "تلقائي";
"Scene.Settings.Section.Appearance.Dark" = "مظلمٌ دائِمًا";
@@ -335,12 +337,12 @@ uploaded to Mastodon.";
"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.FollowExplain" = "عِندَ مُتابَعَتِكَ لأحدِهِم، سَوف تَرى مَنشوراته في تغذيَتِكَ الرئيسة.";
"Scene.SuggestionAccount.Title" = "ابحث عن أشخاص لمتابعتهم";
"Scene.Thread.BackTitle" = "منشور";
"Scene.Thread.Title" = "مَنشور مِن %@";
-"Scene.Welcome.Slogan" = "Social networking
-back in your hands.";
+"Scene.Welcome.Slogan" = "شبكات التواصل الاجتماعي
+مرة أُخرى بين يديك.";
"Scene.Wizard.AccessibilityHint" = "انقر نقرًا مزدوجًا لتجاهل النافذة المنبثقة";
-"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Switch between multiple accounts by holding the profile button.";
+"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "بدِّل بين حسابات متعددة عبر الاستمرار بالضغط على زر الملف الشخصي.";
"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 e3dee0d80..0b28c577a 100644
--- a/Mastodon/Resources/ar.lproj/Localizable.stringsdict
+++ b/Mastodon/Resources/ar.lproj/Localizable.stringsdict
@@ -13,13 +13,13 @@
NSStringFormatValueTypeKey
ld
zero
- %ld unread notification
+ لا إشعار غير مقروء
one
إشعار واحِد غير مقروء
two
إشعاران غير مقروءان
few
- %ld unread notification
+ %ld إشعارات غير مقروءة
many
%ld إشعارًا غيرَ مقروء
other
@@ -277,7 +277,7 @@
NSStringFormatValueTypeKey
ld
zero
- %ld followers
+ لا مُتابِع
one
مُتابِعٌ واحد
two
diff --git a/Mastodon/Resources/ca.lproj/Localizable.strings b/Mastodon/Resources/ca.lproj/Localizable.strings
index 71eb62b00..1642fc8a5 100644
--- a/Mastodon/Resources/ca.lproj/Localizable.strings
+++ b/Mastodon/Resources/ca.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Comprova la teva connexió a Internet.";
"Common.Controls.Actions.Back" = "Enrere";
"Common.Controls.Actions.BlockDomain" = "Bloqueja %@";
"Common.Controls.Actions.Cancel" = "Cancel·la";
+"Common.Controls.Actions.Compose" = "Composa";
"Common.Controls.Actions.Confirm" = "Confirma";
"Common.Controls.Actions.Continue" = "Continua";
"Common.Controls.Actions.CopyPhoto" = "Copia la foto";
@@ -190,6 +191,8 @@ carregat a Mastodon.";
toca l'enllaç per a confirmar el teu compte.";
"Scene.ConfirmEmail.Title" = "Una última cosa.";
"Scene.Favorite.Title" = "Els teus Favorits";
+"Scene.Follower.Footer" = "Els seguidors d'altres servidors no son mostrats.";
+"Scene.Following.Footer" = "Els seguits d'altres servidors no son mostrats.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Veure noves publicacions";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Fora de línia";
"Scene.HomeTimeline.NavigationBarState.Published" = "Publicat!";
diff --git a/Mastodon/Resources/de.lproj/Localizable.strings b/Mastodon/Resources/de.lproj/Localizable.strings
index 2780723ed..12fba5387 100644
--- a/Mastodon/Resources/de.lproj/Localizable.strings
+++ b/Mastodon/Resources/de.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Bitte überprüfe deine Internetverbindung.";
"Common.Controls.Actions.Back" = "Zurück";
"Common.Controls.Actions.BlockDomain" = "%@ blockieren";
"Common.Controls.Actions.Cancel" = "Abbrechen";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "Bestätigen";
"Common.Controls.Actions.Continue" = "Fortfahren";
"Common.Controls.Actions.CopyPhoto" = "Foto kopieren";
@@ -190,6 +191,8 @@ kann nicht auf Mastodon hochgeladen werden.";
tippe darin auf den Link, um Dein Konto zu bestätigen.";
"Scene.ConfirmEmail.Title" = "Noch eine letzte Sache.";
"Scene.Favorite.Title" = "Deine Favoriten";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Neue Beiträge anzeigen";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Veröffentlicht!";
diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings
index a8852675c..0f3ed66ae 100644
--- a/Mastodon/Resources/en.lproj/Localizable.strings
+++ b/Mastodon/Resources/en.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Please check your internet connection.";
"Common.Controls.Actions.Back" = "Back";
"Common.Controls.Actions.BlockDomain" = "Block %@";
"Common.Controls.Actions.Cancel" = "Cancel";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "Confirm";
"Common.Controls.Actions.Continue" = "Continue";
"Common.Controls.Actions.CopyPhoto" = "Copy Photo";
@@ -190,6 +191,8 @@ uploaded to Mastodon.";
tap the link to confirm your account.";
"Scene.ConfirmEmail.Title" = "One last thing.";
"Scene.Favorite.Title" = "Your Favorites";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
diff --git a/Mastodon/Resources/es-419.lproj/Localizable.strings b/Mastodon/Resources/es-419.lproj/Localizable.strings
index 209502b26..cf97fe803 100644
--- a/Mastodon/Resources/es-419.lproj/Localizable.strings
+++ b/Mastodon/Resources/es-419.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Por favor, revisá tu conexión a Internet.";
"Common.Controls.Actions.Back" = "Volver";
"Common.Controls.Actions.BlockDomain" = "Bloquear a %@";
"Common.Controls.Actions.Cancel" = "Cancelar";
+"Common.Controls.Actions.Compose" = "Redactar";
"Common.Controls.Actions.Confirm" = "Confirmar";
"Common.Controls.Actions.Continue" = "Continuar";
"Common.Controls.Actions.CopyPhoto" = "Copiar foto";
@@ -190,6 +191,8 @@ y no se puede subir a Mastodon.";
pulsá en el enlace para confirmar tu cuenta.";
"Scene.ConfirmEmail.Title" = "Una última cosa.";
"Scene.Favorite.Title" = "Tus favoritos";
+"Scene.Follower.Footer" = "No se muestran los seguidores de otros servidores.";
+"Scene.Following.Footer" = "No se muestran las cuentas de otros servidores que seguís.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Ver nuevos mensajes";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Desconectado";
"Scene.HomeTimeline.NavigationBarState.Published" = "¡Enviado!";
diff --git a/Mastodon/Resources/es.lproj/Localizable.strings b/Mastodon/Resources/es.lproj/Localizable.strings
index 1fffc928a..dcf12dfe2 100644
--- a/Mastodon/Resources/es.lproj/Localizable.strings
+++ b/Mastodon/Resources/es.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Por favor, revise su conexión a internet.";
"Common.Controls.Actions.Back" = "Atrás";
"Common.Controls.Actions.BlockDomain" = "Bloquear %@";
"Common.Controls.Actions.Cancel" = "Cancelar";
+"Common.Controls.Actions.Compose" = "Redactar";
"Common.Controls.Actions.Confirm" = "Confirmar";
"Common.Controls.Actions.Continue" = "Continuar";
"Common.Controls.Actions.CopyPhoto" = "Copiar foto";
@@ -190,6 +191,8 @@ subirse a Mastodon.";
pulsa en el enlace para confirmar tu cuenta.";
"Scene.ConfirmEmail.Title" = "Una última cosa.";
"Scene.Favorite.Title" = "Tus Favoritos";
+"Scene.Follower.Footer" = "No se muestran los seguidores de otros servidores.";
+"Scene.Following.Footer" = "No se muestran los seguidos de otros servidores.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Ver nuevas publicaciones";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Sin Conexión";
"Scene.HomeTimeline.NavigationBarState.Published" = "¡Publicado!";
diff --git a/Mastodon/Resources/fr.lproj/Localizable.strings b/Mastodon/Resources/fr.lproj/Localizable.strings
index 76df6b600..a4dbfdb6f 100644
--- a/Mastodon/Resources/fr.lproj/Localizable.strings
+++ b/Mastodon/Resources/fr.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Veuillez vérifier votre accès à Internet.";
"Common.Controls.Actions.Back" = "Retour";
"Common.Controls.Actions.BlockDomain" = "Bloquer %@";
"Common.Controls.Actions.Cancel" = "Annuler";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "Confirmer";
"Common.Controls.Actions.Continue" = "Continuer";
"Common.Controls.Actions.CopyPhoto" = "Copier la photo";
@@ -133,7 +134,7 @@ Votre profil ressemble à ça pour lui.";
"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Chargement des publications manquantes...";
"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Charger plus de réponses";
"Common.Controls.Timeline.Timestamp.Now" = "À l’instant";
-"Scene.AccountList.AddAccount" = "Add Account";
+"Scene.AccountList.AddAccount" = "Ajouter un compte";
"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher";
"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher";
"Scene.Compose.Accessibility.AppendAttachment" = "Joindre un document";
@@ -190,6 +191,8 @@ téléversé sur Mastodon.";
tapotez le lien pour confirmer votre compte.";
"Scene.ConfirmEmail.Title" = "Une dernière chose.";
"Scene.Favorite.Title" = "Vos favoris";
+"Scene.Follower.Footer" = "Les abonné·e·s issus des autres serveurs ne sont pas affiché·e·s.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Voir les nouvelles publications";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Hors ligne";
"Scene.HomeTimeline.NavigationBarState.Published" = "Publié!";
@@ -340,6 +343,6 @@ n'importe quel serveur.";
"Scene.Thread.BackTitle" = "Publication";
"Scene.Thread.Title" = "Publication de %@";
"Scene.Welcome.Slogan" = "Le réseau social qui vous rend le contrôle.";
-"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" = "Tapotez deux fois pour fermer cet assistant";
+"Scene.Wizard.MultipleAccountSwitchIntroDescription" = "Basculez entre plusieurs comptes en appuyant de maniere prolongée sur le bouton profil.";
+"Scene.Wizard.NewInMastodon" = "Nouveau dans Mastodon";
\ No newline at end of file
diff --git a/Mastodon/Resources/fr.lproj/Localizable.stringsdict b/Mastodon/Resources/fr.lproj/Localizable.stringsdict
index d512b204c..4a912e4b3 100644
--- a/Mastodon/Resources/fr.lproj/Localizable.stringsdict
+++ b/Mastodon/Resources/fr.lproj/Localizable.stringsdict
@@ -13,15 +13,15 @@
NSStringFormatValueTypeKey
ld
one
- 1 unread notification
+ 1 notification non lue
other
- %ld unread notification
+ %ld notifications non lues
a11y.plural.count.input_limit_exceeds
NSStringLocalizedFormatKey
- Input limit exceeds %#@character_count@
+ La limite d’entrée dépasse %#@character_count@
character_count
NSStringFormatSpecTypeKey
diff --git a/Mastodon/Resources/gd-GB.lproj/Localizable.strings b/Mastodon/Resources/gd-GB.lproj/Localizable.strings
index 6c01adb0a..3f80f6411 100644
--- a/Mastodon/Resources/gd-GB.lproj/Localizable.strings
+++ b/Mastodon/Resources/gd-GB.lproj/Localizable.strings
@@ -28,6 +28,7 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon.";
"Common.Controls.Actions.Back" = "Air ais";
"Common.Controls.Actions.BlockDomain" = "Bac %@";
"Common.Controls.Actions.Cancel" = "Sguir dheth";
+"Common.Controls.Actions.Compose" = "Sgrìobh";
"Common.Controls.Actions.Confirm" = "Dearbh";
"Common.Controls.Actions.Continue" = "Lean air adhart";
"Common.Controls.Actions.CopyPhoto" = "Dèan lethbhreac dhen dealbh";
@@ -190,6 +191,8 @@ a luchdadh suas gu Mastodon.";
thoir gnogag air a’ chunntas a dhearbhadh a’ chunntais agad.";
"Scene.ConfirmEmail.Title" = "Aon rud eile.";
"Scene.Favorite.Title" = "Na h-annsachdan agad";
+"Scene.Follower.Footer" = "Cha dèid luchd-leantainn o fhrithealaichean eile a shealltainn.";
+"Scene.Following.Footer" = "Cha dèid cò air a leanas tu air frithealaichean eile a shealltainn.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Seall na postaichean ùra";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Far loidhne";
"Scene.HomeTimeline.NavigationBarState.Published" = "Chaidh fhoillseachadh!";
diff --git a/Mastodon/Resources/ja.lproj/Localizable.strings b/Mastodon/Resources/ja.lproj/Localizable.strings
index beadccf22..98bf71639 100644
--- a/Mastodon/Resources/ja.lproj/Localizable.strings
+++ b/Mastodon/Resources/ja.lproj/Localizable.strings
@@ -28,6 +28,7 @@
"Common.Controls.Actions.Back" = "戻る";
"Common.Controls.Actions.BlockDomain" = "%@をブロック";
"Common.Controls.Actions.Cancel" = "キャンセル";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "確認";
"Common.Controls.Actions.Continue" = "続ける";
"Common.Controls.Actions.CopyPhoto" = "写真をコピー";
@@ -184,6 +185,8 @@
"Scene.ConfirmEmail.Subtitle" = "先程 %@ にメールを送信しました。リンクをタップしてアカウントを確認してください。";
"Scene.ConfirmEmail.Title" = "さいごにもうひとつ。";
"Scene.Favorite.Title" = "お気に入り";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "新しい投稿を見る";
"Scene.HomeTimeline.NavigationBarState.Offline" = "オフライン";
"Scene.HomeTimeline.NavigationBarState.Published" = "投稿しました!";
diff --git a/Mastodon/Resources/ku-TR.lproj/Localizable.strings b/Mastodon/Resources/ku-TR.lproj/Localizable.strings
index 345f10cf9..d0d0f294e 100644
--- a/Mastodon/Resources/ku-TR.lproj/Localizable.strings
+++ b/Mastodon/Resources/ku-TR.lproj/Localizable.strings
@@ -1,21 +1,21 @@
"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.CleanCache.Message" = "Pêşbîra %@ biserketî hate pakkirin.";
+"Common.Alerts.CleanCache.Title" = "Pêşbîrê pak 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.DiscardPostContent.Message" = "Bipejrîne ku naveroka şandiyê ya hatiye nivîsandin paşguh bikî.";
+"Common.Alerts.DiscardPostContent.Title" = "Reşnivîsê paşguh bike";
"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.EditProfileFailure.Title" = "Di serrastkirina profîlê çewtî";
"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.Message" = "Ji kerema xwe mafê bide gihîştina wênegehê çalak bike da ku wêne werin tomarkirin.";
"Common.Alerts.SavePhotoFailure.Title" = "Tomarkirina wêneyê têkçû";
"Common.Alerts.ServerError.Title" = "Çewtiya rajekar";
"Common.Alerts.SignOut.Confirm" = "Derkeve";
@@ -28,15 +28,16 @@ Jkx girêdana înternetê xwe kontrol bike.";
"Common.Controls.Actions.Back" = "Vegere";
"Common.Controls.Actions.BlockDomain" = "%@ asteng bike";
"Common.Controls.Actions.Cancel" = "Dev jê berde";
+"Common.Controls.Actions.Compose" = "Binivîsîne";
"Common.Controls.Actions.Confirm" = "Bipejirîne";
"Common.Controls.Actions.Continue" = "Bidomîne";
-"Common.Controls.Actions.CopyPhoto" = "Wêne kopî bikin";
+"Common.Controls.Actions.CopyPhoto" = "Wêneyê jê bigire";
"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.FindPeople" = "Mirovan bo şopandinê bibîne";
+"Common.Controls.Actions.ManuallySearch" = "Ji devlê bi destan lêgerînê bike";
"Common.Controls.Actions.Next" = "Pêş";
"Common.Controls.Actions.Ok" = "BAŞ E";
"Common.Controls.Actions.Open" = "Veke";
@@ -47,7 +48,7 @@ Jkx girêdana înternetê xwe kontrol bike.";
"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.SavePhoto" = "Wêneyê tomar bike";
"Common.Controls.Actions.SeeMore" = "Bêtir bibîne";
"Common.Controls.Actions.Settings" = "Sazkarî";
"Common.Controls.Actions.Share" = "Parve bike";
@@ -70,7 +71,7 @@ Jkx girêdana înternetê xwe kontrol 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.Request" = "Daxwaz bike";
"Common.Controls.Friendship.Unblock" = "Astengiyê rake";
"Common.Controls.Friendship.UnblockUser" = "%@ asteng neke";
"Common.Controls.Friendship.Unmute" = "Bêdeng neke";
@@ -79,86 +80,86 @@ Jkx girêdana înternetê xwe kontrol bike.";
"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.SegmentedControl.NextSection" = "Beşa pêş";
+"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Beşa paş";
"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.PreviewImage" = "Pêşdîtina wêneyê";
"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.Keyboard.Timeline.ToggleContentWarning" = "Hişyariya naverokê biguherîne";
+"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Li ser şandiyê bijarte biguherîne";
+"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Ji vû nivîsandin di şandiyê de biguherîne";
+"Common.Controls.Status.Actions.Favorite" = "Bijarte";
+"Common.Controls.Status.Actions.Menu" = "Kulîn";
+"Common.Controls.Status.Actions.Reblog" = "Ji nû ve nivîsandin";
"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.Actions.Unfavorite" = "Nebijarte";
+"Common.Controls.Status.Actions.Unreblog" = "Ji nû ve nivîsandinê vegere";
"Common.Controls.Status.ContentWarning" = "Hişyariya naverokê";
-"Common.Controls.Status.MediaContentWarning" = "Ji bo aşkerakirinê derekî bitikîne";
+"Common.Controls.Status.MediaContentWarning" = "Ji bo eşkerekirinê li derekî bitikîne";
"Common.Controls.Status.Poll.Closed" = "Girtî";
-"Common.Controls.Status.Poll.Vote" = "Deng";
+"Common.Controls.Status.Poll.Vote" = "Deng bide";
"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.Emoji" = "Emojî";
+"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
"Common.Controls.Status.Tag.Link" = "Girêdan";
-"Common.Controls.Status.Tag.Mention" = "Behs";
+"Common.Controls.Status.Tag.Mention" = "Qalkirin";
"Common.Controls.Status.Tag.Url" = "URL";
-"Common.Controls.Status.UserReblogged" = "%@ ji nû ve hat blogkirin";
+"Common.Controls.Status.UserReblogged" = "%@ ji nû ve hate nivîsandin";
"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î.
+"Common.Controls.Timeline.Header.BlockedWarning" = "Tu nikarî profîla vî/ê bikarhênerî bibînî
+heya ku ew astengiyê li ser te rakin.";
+"Common.Controls.Timeline.Header.BlockingWarning" = "Tu nikarî profîla vî/ê bikarhênerî bibînî
+Heya ku tu astengiyê li ser wî/ê ranekî.
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.NoStatusFound" = "Tu şandî nehate dîtin";
+"Common.Controls.Timeline.Header.SuspendedWarning" = "Ev bikarhêner hatiye rawestandin.";
"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î.
+Heya ku tu astengiyê li ser wî/ê ranekî.
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.Header.UserSuspendedWarning" = "Ajimêra %@ hatiye rawestandin.";
+"Common.Controls.Timeline.Loader.LoadMissingPosts" = "Şandiyên wendayî bar bike";
+"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Şandiyên wendayî tên barkirin...";
"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.DismissAccountSwitcher" = "Guherkera ajimêrê paş guh bike";
"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.CustomEmojiPicker" = "Hilbijêrê emojî yên kesanekirî";
"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.EnableContentWarning" = "Hişyariya naverokê çalak bike";
+"Scene.Compose.Accessibility.PostVisibilityMenu" = "Kulîna 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.DescriptionPhoto" = "Wêneyê ji bo kêmbînên dîtbar bide nasîn...";
+"Scene.Compose.Attachment.DescriptionVideo" = "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...";
"Scene.Compose.Attachment.Photo" = "wêne";
"Scene.Compose.Attachment.Video" = "vîdyo";
-"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
+"Scene.Compose.AutoComplete.SpaceToAdd" = "Bicîhkirinê tevlî bike";
"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.ContentInputPlaceholder" = "Tiştê ku di hişê te de ye binivîsin an jî pêve bike";
+"Scene.Compose.ContentWarning.Placeholder" = "Li vir hişyariyek hûrgilî binivîsine...";
+"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Pêvek tevlî bike - %@";
+"Scene.Compose.Keyboard.DiscardPost" = "Şandî paşguh bike";
+"Scene.Compose.Keyboard.PublishPost" = "Şandiyê biweşîne";
+"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Xuyabûnê hilbijêre - %@";
+"Scene.Compose.Keyboard.ToggleContentWarning" = "Hişyariya naverokê biguherîne";
+"Scene.Compose.Keyboard.TogglePoll" = "Rapirsiyê biguherîne";
"Scene.Compose.MediaSelection.Browse" = "Bigere";
"Scene.Compose.MediaSelection.Camera" = "Wêne bikişîne";
"Scene.Compose.MediaSelection.PhotoLibrary" = "Wênegeh";
@@ -182,98 +183,100 @@ Profîla te ji wan ra wiha xuya dike.";
"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.Description" = "Me tenê ji te re e-nameyek şand. Heke nehatiye peldanka xwe ya spamê kontrol bike.";
"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.OpenEmailApp.Title" = "Nameyên xwe yên wergirtî kontrol bike.";
+"Scene.ConfirmEmail.Subtitle" = "Me tenê e-nameyek ji %@ re şand,
+girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin.";
"Scene.ConfirmEmail.Title" = "Tiştekî dawî.";
-"Scene.Favorite.Title" = "Bijareyên te";
+"Scene.Favorite.Title" = "Bijarteyên te";
+"Scene.Follower.Footer" = "Şopîner ji rajekerên din nayê dîtin.";
+"Scene.Following.Footer" = "Şopandin ji rajekerên din nayê dîtin.";
"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.Keyobard.ShowMentions" = "Qalkirinan 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.Title.Mentions" = "Qalkirin";
+"Scene.Notification.UserFavorited Your Post" = "%@ şandiya te hez kir";
"Scene.Notification.UserFollowedYou" = "%@ te şopand";
-"Scene.Notification.UserMentionedYou" = "%@ behsa te kir";
+"Scene.Notification.UserMentionedYou" = "%@ qale 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.Notification.UserRequestedToFollowYou" = "%@ dixwazê te bişopîne";
+"Scene.Notification.UserYourPollHasEnded" = "Rapirsîya 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.Followers" = "şopîner";
"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.Dashboard.Posts" = "şandî";
+"Scene.Profile.Fields.AddRow" = "Rêzê tevlî 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.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Ji bo rakirina astengkirinê bipejirîne %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Astengiyê li ser ajimêr rake";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Ji bo vekirina bêdengkirinê bipejirîne %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Ajimêrê bêdeng neke";
"Scene.Profile.SegmentedControl.Media" = "Medya";
-"Scene.Profile.SegmentedControl.Posts" = "Şandîyan";
-"Scene.Profile.SegmentedControl.Replies" = "Bersivan";
-"Scene.Register.Error.Item.Agreement" = "Lihevhatin";
+"Scene.Profile.SegmentedControl.Posts" = "Şandî";
+"Scene.Profile.SegmentedControl.Replies" = "Bersiv";
+"Scene.Register.Error.Item.Agreement" = "Peyman";
"Scene.Register.Error.Item.Email" = "E-name";
-"Scene.Register.Error.Item.Locale" = "Herêm";
-"Scene.Register.Error.Item.Password" = "Şîfre";
+"Scene.Register.Error.Item.Locale" = "Zimanê navrûyê";
+"Scene.Register.Error.Item.Password" = "Pêborîn";
"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.Accepted" = "%@ divê were pejirandin";
"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.Blocked" = "%@ peydekerê e-peyamê yê qedexekirî dihewîne";
+"Scene.Register.Error.Reason.Inclusion" = "%@ ne nirxek piştgirî ye";
"Scene.Register.Error.Reason.Invalid" = "%@ ne derbasdar e";
-"Scene.Register.Error.Reason.Reserved" = "%@ peyveke mifteya veqetandî ye";
+"Scene.Register.Error.Reason.Reserved" = "%@ peyveke parastî ye";
"Scene.Register.Error.Reason.Taken" = "%@ jixwe tê bikaranîn";
-"Scene.Register.Error.Reason.TooLong" = "%@ gelekî dirêj e";
+"Scene.Register.Error.Reason.TooLong" = "%@ pir 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.Reason.Unreachable" = "%@ xuya ye ku tune ye";
+"Scene.Register.Error.Special.EmailInvalid" = "Ev navnîşaneke e-nameyê ne derbasdar e";
+"Scene.Register.Error.Special.PasswordTooShort" = "Pêborîn pir kurt e (divê herî kêm 8 tîp be)";
+"Scene.Register.Error.Special.UsernameInvalid" = "Navê bikarhêner divê tenê ji tîpên alfajimarî û binxêz pêk be";
"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.Password.Hint" = "Pêborîna te herî kêm divê ji 8 tîpan pêk bê";
+"Scene.Register.Input.Password.Placeholder" = "pêborîn";
"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.Content1" = "Şandiyên din hene ku tu dixwazî tevlî ragihandinê bikî?";
+"Scene.Report.Content2" = "Derbarê vê ragihandinê de tiştek heye ku divê çavdêr bizanin?";
"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.TextPlaceholder" = "Şiroveyên daxwazkirê binivîsine an jî pê ve bike";
"Scene.Report.Title" = "%@ ragihîne";
-"Scene.Search.Recommend.Accounts.Description" = "Dibe ku tu bixwazî van hesaban bişopînî";
+"Scene.Search.Recommend.Accounts.Description" = "Dibe ku tu bixwazî van ajimêran 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.Accounts.Title" = "Ajimêrên ku belkî tu jê hez bikî";
+"Scene.Search.Recommend.ButtonText" = "Hemûyan bibîne";
+"Scene.Search.Recommend.HashTag.Description" = "Hashtag ê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.Recommend.HashTag.Title" = "Rojev li ser Mastodon";
+"Scene.Search.SearchBar.Cancel" = "Dev jê berde";
+"Scene.Search.SearchBar.Placeholder" = "Li hashtag û bikarhêneran bigere";
+"Scene.Search.Searching.Clear" = "Pak 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.Hashtags" = "Hashtag";
"Scene.Search.Searching.Segment.People" = "Mirov";
-"Scene.Search.Searching.Segment.Posts" = "Şandîyan";
+"Scene.Search.Searching.Segment.Posts" = "Şandî";
"Scene.Search.Title" = "Bigere";
"Scene.ServerPicker.Button.Category.Academia" = "akademî";
"Scene.ServerPicker.Button.Category.Activism" = "çalakî";
@@ -291,34 +294,34 @@ tap the link to confirm your account.";
"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.EmptyState.BadNetwork" = "Di dema barkirina daneyan da çewtî derket. Girêdana xwe ya înternetê kontrol bike.";
+"Scene.ServerPicker.EmptyState.FindingServers" = "Peydakirina rajekarên berdest...";
+"Scene.ServerPicker.EmptyState.NoResults" = "Encam tune";
+"Scene.ServerPicker.Input.Placeholder" = "Rajekarekî bibîne an jî beşdarî ya xwe bibe...";
+"Scene.ServerPicker.Label.Category" = "BEŞ";
"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.Button.Confirm" = "Ez dipejirînim";
+"Scene.ServerRules.PrivacyPolicy" = "polîtikaya nihêniyê";
+"Scene.ServerRules.Prompt" = "Bi domandinê, tu ji bo %@ di bin mercên bikaranînê û polîtîkaya nepenîtiyê dipejirînî.";
"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.ServerRules.TermsOfService" = "mercên bikaranînê";
+"Scene.ServerRules.Title" = "Hinek rêzikên bingehîn.";
+"Scene.Settings.Footer.MastodonDescription" = "Mastodon nermalava çavkaniya vekirî ye. Tu dikarî pirsgirêkan li ser GitHub-ê ragihînî di %@ (%@) de";
+"Scene.Settings.Keyboard.CloseSettingsWindow" = "Sazkariyên çarçoveyê bigire";
"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.BoringZone.AccountSettings" = "Sazkariyên ajimêr";
+"Scene.Settings.Section.BoringZone.Privacy" = "Polîtikaya nihêniyê";
+"Scene.Settings.Section.BoringZone.Terms" = "Mercên bikaranînê";
+"Scene.Settings.Section.BoringZone.Title" = "Devera acizker";
+"Scene.Settings.Section.Notifications.Boosts" = "Şandiya min ji nû ve nivîsand";
"Scene.Settings.Section.Notifications.Favorites" = "Şandiyên min hez kir";
-"Scene.Settings.Section.Notifications.Follows" = "Min şopand";
+"Scene.Settings.Section.Notifications.Follows" = "Min dişopîne";
"Scene.Settings.Section.Notifications.Mentions" = "Qale min kir";
"Scene.Settings.Section.Notifications.Title" = "Agahdarî";
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "her kes";
@@ -326,21 +329,21 @@ Her kîjan rajekar be.";
"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.DisableAvatarAnimation" = "Avatarên anîmasyonî neçalak bike";
+"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Emojiyên anîmasyonî neçalak bike";
"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.Section.Preference.TrueBlackDarkMode" = "Moda tarî ya reş a rastîn";
+"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Ji bo vekirina girêdanan geroka berdest bi kar bîne";
+"Scene.Settings.Section.SpicyZone.Clear" = "Pêşbîra medyayê pak bike";
+"Scene.Settings.Section.SpicyZone.Signout" = "Derkeve";
+"Scene.Settings.Section.SpicyZone.Title" = "Devera germ";
"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.Thread.Title" = "Şandî ji %@";
"Scene.Welcome.Slogan" = "Torên civakî
di destên te de.";
-"Scene.Wizard.AccessibilityHint" = "Double tap to dismiss this wizard";
+"Scene.Wizard.AccessibilityHint" = "Du caran bitikîne da ku çarçoveyahilpekok ji holê rakî";
"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
index 064b8bf2b..8ae1b812a 100644
--- a/Mastodon/Resources/ku-TR.lproj/Localizable.stringsdict
+++ b/Mastodon/Resources/ku-TR.lproj/Localizable.stringsdict
@@ -109,9 +109,9 @@
NSStringFormatValueTypeKey
ld
one
- 1 reblog
+ 1 ji nû ve nivîsandin
other
- %ld reblogs
+ %ld ji nû ve nivîsandin
plural.count.vote
diff --git a/Mastodon/Resources/local-codes.json b/Mastodon/Resources/local-codes.json
new file mode 100644
index 000000000..949649791
--- /dev/null
+++ b/Mastodon/Resources/local-codes.json
@@ -0,0 +1,80 @@
+{
+ "af": "Afrikaans",
+ "ar": "العربية",
+ "ast": "Asturianu",
+ "bg": "Български",
+ "bn": "বাংলা",
+ "br": "Breton",
+ "ca": "Català",
+ "co": "Corsu",
+ "cs": "Čeština",
+ "cy": "Cymraeg",
+ "da": "Dansk",
+ "de": "Deutsch",
+ "el": "Ελληνικά",
+ "en": "English",
+ "eo": "Esperanto",
+ "es-AR": "Español (Argentina)",
+ "es": "Español",
+ "et": "Eesti",
+ "eu": "Euskara",
+ "fa": "فارسی",
+ "fi": "Suomi",
+ "fr": "Français",
+ "ga": "Gaeilge",
+ "gd": "Gàidhlig",
+ "gl": "Galego",
+ "he": "עברית",
+ "hi": "हिन्दी",
+ "hr": "Hrvatski",
+ "hu": "Magyar",
+ "hy": "Հայերեն",
+ "id": "Bahasa Indonesia",
+ "io": "Ido",
+ "is": "Íslenska",
+ "it": "Italiano",
+ "ja": "日本語",
+ "ka": "ქართული",
+ "kab": "Taqbaylit",
+ "kk": "Қазақша",
+ "kn": "ಕನ್ನಡ",
+ "ko": "한국어",
+ "ku": "سۆرانی",
+ "lt": "Lietuvių",
+ "lv": "Latviešu",
+ "mk": "Македонски",
+ "ml": "മലയാളം",
+ "mr": "मराठी",
+ "ms": "Bahasa Melayu",
+ "nl": "Nederlands",
+ "nn": "Nynorsk",
+ "no": "Norsk",
+ "oc": "Occitan",
+ "pl": "Polski",
+ "pt-BR": "Português (Brasil)",
+ "pt-PT": "Português (Portugal)",
+ "pt": "Português",
+ "ro": "Română",
+ "ru": "Русский",
+ "sa": "संस्कृतम्",
+ "sc": "Sardu",
+ "si": "සිංහල",
+ "sk": "Slovenčina",
+ "sl": "Slovenščina",
+ "sq": "Shqip",
+ "sr-Latn": "Srpski (latinica)",
+ "sr": "Српски",
+ "sv": "Svenska",
+ "ta": "தமிழ்",
+ "te": "తెలుగు",
+ "th": "ไทย",
+ "tr": "Türkçe",
+ "uk": "Українська",
+ "ur": "اُردُو",
+ "vi": "Tiếng Việt",
+ "zgh": "ⵜⴰⵎⴰⵣⵉⵖⵜ",
+ "zh-CN": "简体中文",
+ "zh-HK": "繁體中文(香港)",
+ "zh-TW": "繁體中文(臺灣)",
+ "zh": "中文"
+}
\ No newline at end of file
diff --git a/Mastodon/Resources/nl.lproj/Localizable.strings b/Mastodon/Resources/nl.lproj/Localizable.strings
index 1ebda1172..9c84e138f 100644
--- a/Mastodon/Resources/nl.lproj/Localizable.strings
+++ b/Mastodon/Resources/nl.lproj/Localizable.strings
@@ -27,6 +27,7 @@
"Common.Controls.Actions.Back" = "Terug";
"Common.Controls.Actions.BlockDomain" = "Blokkeer %@";
"Common.Controls.Actions.Cancel" = "Annuleren";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "Bevestigen";
"Common.Controls.Actions.Continue" = "Doorgaan";
"Common.Controls.Actions.CopyPhoto" = "Foto kopiëren";
@@ -184,6 +185,8 @@ Uw profiel ziet er zo uit voor hen.";
klik op de link om uw account te bevestigen.";
"Scene.ConfirmEmail.Title" = "Nog één ding.";
"Scene.Favorite.Title" = "Uw favorieten";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Bekijk nieuwe berichten";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
"Scene.HomeTimeline.NavigationBarState.Published" = "Gepubliceerd!";
diff --git a/Mastodon/Resources/ru.lproj/Localizable.strings b/Mastodon/Resources/ru.lproj/Localizable.strings
index 3a8adbbab..1a4f92fc6 100644
--- a/Mastodon/Resources/ru.lproj/Localizable.strings
+++ b/Mastodon/Resources/ru.lproj/Localizable.strings
@@ -28,6 +28,7 @@
"Common.Controls.Actions.Back" = "Назад";
"Common.Controls.Actions.BlockDomain" = "Заблокировать %@";
"Common.Controls.Actions.Cancel" = "Отмена";
+"Common.Controls.Actions.Compose" = "Compose";
"Common.Controls.Actions.Confirm" = "Подтвердить";
"Common.Controls.Actions.Continue" = "Продолжить";
"Common.Controls.Actions.CopyPhoto" = "Скопировать изображение";
@@ -200,6 +201,8 @@
подтвердить свою учётную запись.";
"Scene.ConfirmEmail.Title" = "И ещё кое-что.";
"Scene.Favorite.Title" = "Ваше избранное";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Показать новые";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Не в сети";
"Scene.HomeTimeline.NavigationBarState.Published" = "Опубликовано!";
diff --git a/Mastodon/Resources/th.lproj/Localizable.strings b/Mastodon/Resources/th.lproj/Localizable.strings
index a61b1d15f..1bd954fe5 100644
--- a/Mastodon/Resources/th.lproj/Localizable.strings
+++ b/Mastodon/Resources/th.lproj/Localizable.strings
@@ -28,6 +28,7 @@
"Common.Controls.Actions.Back" = "ย้อนกลับ";
"Common.Controls.Actions.BlockDomain" = "ปิดกั้น %@";
"Common.Controls.Actions.Cancel" = "ยกเลิก";
+"Common.Controls.Actions.Compose" = "เขียน";
"Common.Controls.Actions.Confirm" = "ยืนยัน";
"Common.Controls.Actions.Continue" = "ดำเนินการต่อ";
"Common.Controls.Actions.CopyPhoto" = "คัดลอกรูปภาพ";
@@ -190,6 +191,8 @@
แตะที่ลิงก์เพื่อยืนยันบัญชีของคุณ";
"Scene.ConfirmEmail.Title" = "หนึ่งสิ่งสุดท้าย";
"Scene.Favorite.Title" = "รายการโปรดของคุณ";
+"Scene.Follower.Footer" = "ไม่ได้แสดงผู้ติดตามจากเซิร์ฟเวอร์อื่น ๆ";
+"Scene.Following.Footer" = "ไม่ได้แสดงการติดตามจากเซิร์ฟเวอร์อื่น ๆ";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "ดูโพสต์ใหม่";
"Scene.HomeTimeline.NavigationBarState.Offline" = "ออฟไลน์";
"Scene.HomeTimeline.NavigationBarState.Published" = "เผยแพร่แล้ว!";
diff --git a/Mastodon/Resources/zh-Hans.lproj/Localizable.strings b/Mastodon/Resources/zh-Hans.lproj/Localizable.strings
index 21c41a15d..7a6b02032 100644
--- a/Mastodon/Resources/zh-Hans.lproj/Localizable.strings
+++ b/Mastodon/Resources/zh-Hans.lproj/Localizable.strings
@@ -28,6 +28,7 @@
"Common.Controls.Actions.Back" = "返回";
"Common.Controls.Actions.BlockDomain" = "屏蔽 %@";
"Common.Controls.Actions.Cancel" = "取消";
+"Common.Controls.Actions.Compose" = "撰写";
"Common.Controls.Actions.Confirm" = "确认";
"Common.Controls.Actions.Continue" = "继续";
"Common.Controls.Actions.CopyPhoto" = "拷贝照片";
@@ -190,6 +191,8 @@
点击链接确认你的帐户。";
"Scene.ConfirmEmail.Title" = "最后一件事。";
"Scene.Favorite.Title" = "你的喜欢";
+"Scene.Follower.Footer" = "不会显示来自其它服务器的关注者";
+"Scene.Following.Footer" = "不会显示来自其它服务器的关注";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "查看新帖子";
"Scene.HomeTimeline.NavigationBarState.Offline" = "离线";
"Scene.HomeTimeline.NavigationBarState.Published" = "已发送";
diff --git a/Mastodon/Scene/Account/AccountViewController.swift b/Mastodon/Scene/Account/AccountViewController.swift
index 4f2ece253..fce9c7320 100644
--- a/Mastodon/Scene/Account/AccountViewController.swift
+++ b/Mastodon/Scene/Account/AccountViewController.swift
@@ -75,7 +75,7 @@ extension AccountListViewController {
override func viewDidLoad() {
super.viewDidLoad()
- view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor.withAlphaComponent(0.9)
+ setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
@@ -131,7 +131,15 @@ extension AccountListViewController {
}
private func setupBackgroundColor(theme: Theme) {
- view.backgroundColor = theme.systemBackgroundColor.withAlphaComponent(0.9)
+ let backgroundColor = UIColor { traitCollection in
+ switch traitCollection.userInterfaceLevel {
+ case .elevated where traitCollection.userInterfaceStyle == .dark:
+ return theme.systemElevatedBackgroundColor
+ default:
+ return theme.systemBackgroundColor.withAlphaComponent(0.9)
+ }
+ }
+ view.backgroundColor = backgroundColor
}
}
diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
index b8f7d5381..f6ab75877 100644
--- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
+++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
@@ -12,6 +12,7 @@ import MetaTextKit
final class AccountListTableViewCell: UITableViewCell {
+ private var _disposeBag = Set()
var disposeBag = Set()
let avatarButton = CircleAvatarButton(frame: .zero)
@@ -47,6 +48,15 @@ final class AccountListTableViewCell: UITableViewCell {
extension AccountListTableViewCell {
private func _init() {
+ backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
+ }
+ .store(in: &_disposeBag)
+
avatarButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(avatarButton)
NSLayoutConstraint.activate([
diff --git a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift
index 722896641..0873c1390 100644
--- a/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift
+++ b/Mastodon/Scene/Account/Cell/AddAccountTableViewCell.swift
@@ -6,10 +6,13 @@
//
import UIKit
+import Combine
import MetaTextKit
final class AddAccountTableViewCell: UITableViewCell {
+ private var _disposeBag = Set()
+
let iconImageView: UIImageView = {
let image = UIImage(systemName: "plus.circle.fill")!
let imageView = UIImageView(image: image)
@@ -41,6 +44,15 @@ final class AddAccountTableViewCell: UITableViewCell {
extension AddAccountTableViewCell {
private func _init() {
+ backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
+ ThemeService.shared.currentTheme
+ .receive(on: RunLoop.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
+ }
+ .store(in: &_disposeBag)
+
iconImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(iconImageView)
NSLayoutConstraint.activate([
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
index 6d79d0603..6e75a17e7 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift
@@ -15,6 +15,7 @@ import FLEX
import SwiftUI
import MastodonUI
import MastodonSDK
+import StoreKit
extension HomeTimelineViewController {
var debugMenu: UIMenu {
@@ -77,6 +78,11 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.showThreadAction(action)
},
+ UIAction(title: "Store Rating", image: UIImage(systemName: "star.fill"), attributes: []) { [weak self] action in
+ guard let self = self else { return }
+ guard let windowScene = self.view.window?.windowScene else { return }
+ SKStoreReviewController.requestReview(in: windowScene)
+ },
]
)
}
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index 6b5476885..62695f211 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -14,6 +14,7 @@ import CoreDataStack
import GameplayKit
import MastodonSDK
import AlamofireImage
+import StoreKit
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@@ -144,6 +145,21 @@ extension HomeTimelineViewController {
}
.store(in: &disposeBag)
+ viewModel.homeTimelineNavigationBarTitleViewModel.state
+ .removeDuplicates()
+ .filter { $0 == .publishedButton }
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ guard UserDefaults.shared.lastVersionPromptedForReview == nil else { return }
+ guard UserDefaults.shared.processCompletedCount > 3 else { return }
+ guard let windowScene = self.view.window?.windowScene else { return }
+ let version = UIApplication.appVersion()
+ UserDefaults.shared.lastVersionPromptedForReview = version
+ SKStoreReviewController.requestReview(in: windowScene)
+ }
+ .store(in: &disposeBag)
+
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
diff --git a/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift b/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift
index 127cca1b9..57272404e 100644
--- a/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift
+++ b/Mastodon/Scene/Notification/NotificationViewController+StatusProvider.swift
@@ -19,21 +19,25 @@ extension NotificationViewController: StatusProvider {
func status(for cell: UITableViewCell?, indexPath: IndexPath?) -> Future {
return Future { promise in
- guard let cell = cell,
- let diffableDataSource = self.viewModel.diffableDataSource,
- let indexPath = self.tableView.indexPath(for: cell),
+ guard let diffableDataSource = self.viewModel.diffableDataSource else {
+ assertionFailure()
+ promise(.success(nil))
+ return
+ }
+ guard let indexPath = indexPath ?? cell.flatMap({ self.tableView.indexPath(for: $0) }),
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
promise(.success(nil))
return
}
switch item {
- case .notification(let objectID, _):
+ case .notification(let objectID, _),
+ .notificationStatus(let objectID, _):
self.viewModel.fetchedResultsController.managedObjectContext.perform {
let notification = self.viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! MastodonNotification
promise(.success(notification.status))
}
- default:
+ case .bottomLoader:
promise(.success(nil))
}
}
@@ -68,3 +72,6 @@ extension NotificationViewController: StatusProvider {
}
}
+
+// MARK: - UserProvider
+extension NotificationViewController: UserProvider { }
diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift
index 55b4504dc..0567d04dd 100644
--- a/Mastodon/Scene/Notification/NotificationViewController.swift
+++ b/Mastodon/Scene/Notification/NotificationViewController.swift
@@ -14,8 +14,10 @@ import OSLog
import UIKit
import Meta
import MetaTextKit
+import AVKit
-final class NotificationViewController: UIViewController, NeedsDependency {
+final class NotificationViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
+
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
@@ -23,15 +25,18 @@ final class NotificationViewController: UIViewController, NeedsDependency {
var observations = Set()
private(set) lazy var viewModel = NotificationViewModel(context: context)
+
+ let mediaPreviewTransitionController = MediaPreviewTransitionController()
let segmentControl: UISegmentedControl = {
let control = UISegmentedControl(items: [L10n.Scene.Notification.Title.everything, L10n.Scene.Notification.Title.mentions])
- control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.EveryThing.rawValue
+ control.selectedSegmentIndex = NotificationViewModel.NotificationSegment.everyThing.rawValue
return control
}()
let tableView: UITableView = {
let tableView = ControlContainableTableView()
+ tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.self))
tableView.register(NotificationStatusTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationStatusTableViewCell.self))
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
tableView.estimatedRowHeight = UITableView.automaticDimension
@@ -82,7 +87,12 @@ extension NotificationViewController {
tableView.delegate = self
viewModel.tableView = tableView
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
- viewModel.setupDiffableDataSource(for: tableView, delegate: self, dependency: self)
+ viewModel.setupDiffableDataSource(
+ for: tableView,
+ dependency: self,
+ delegate: self,
+ statusTableViewCellDelegate: self
+ )
viewModel.viewDidLoad.send()
// bind refresh control
@@ -128,9 +138,9 @@ extension NotificationViewController {
self.viewModel.needsScrollToTopAfterDataSourceUpdate = true
switch segment {
- case .EveryThing:
+ case .everyThing:
self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID)
- case .Mentions:
+ case .mentions:
self.viewModel.notificationPredicate.value = MastodonNotification.predicate(domain: domain, userID: userID, typeRaw: Mastodon.Entity.Notification.NotificationType.mention.rawValue)
}
}
@@ -148,8 +158,8 @@ extension NotificationViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
-
- tableView.deselectRow(with: transitionCoordinator, animated: animated)
+
+ aspectViewWillAppear(animated)
// fetch latest notification when scroll position is within half screen height to prevent list reload
if tableView.contentOffset.y < view.frame.height * 0.5 {
@@ -181,6 +191,12 @@ extension NotificationViewController {
// reset notification count
context.notificationService.clearNotificationCountForActiveUser()
}
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+
+ aspectViewDidDisappear(animated)
+ }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
@@ -208,33 +224,34 @@ extension NotificationViewController {
}
}
-// MARK: - StatusTableViewControllerAspect
-extension NotificationViewController: StatusTableViewControllerAspect { }
-
-extension NotificationViewController {
-
+// MARK: - TableViewCellHeightCacheableContainer
+extension NotificationViewController: TableViewCellHeightCacheableContainer {
+ var cellFrameCache: NSCache { return viewModel.cellFrameCache }
+
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
- case .notification(let objectID, _):
+ case .notification(let objectID, _),
+ .notificationStatus(let objectID, _):
guard let object = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { return }
- let key = object.id as NSString
+ let key = object.objectID.hashValue
let frame = cell.frame
- viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: key)
+ viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
case .bottomLoader:
break
}
}
-
+
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
switch item {
- case .notification(let objectID, _):
+ case .notification(let objectID, _),
+ .notificationStatus(let objectID, _):
guard let object = try? viewModel.fetchedResultsController.managedObjectContext.existingObject(with: objectID) as? MastodonNotification else { return UITableView.automaticDimension }
- let key = object.id as NSString
- guard let frame = viewModel.cellFrameCache.object(forKey: key)?.cgRectValue else { return UITableView.automaticDimension }
+ let key = object.objectID.hashValue
+ guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: key))?.cgRectValue else { return UITableView.automaticDimension }
return frame.height
case .bottomLoader:
return TimelineLoaderTableViewCell.cellHeight
@@ -242,22 +259,55 @@ extension NotificationViewController {
}
}
+
+// MARK: - StatusTableViewControllerAspect
+extension NotificationViewController: StatusTableViewControllerAspect { }
+
// MARK: - UITableViewDelegate
extension NotificationViewController: UITableViewDelegate {
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
+ aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
+ }
+
+ func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
- open(item: item)
+ switch item {
+ case .notificationStatus:
+ aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
+ case .bottomLoader:
+ if !tableView.isDragging, !tableView.isDecelerating {
+ viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
+ }
+ default:
+ break
+ }
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
- cacheTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
+ aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ aspectTableView(tableView, didSelectRowAt: indexPath)
}
- func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
- return handleTableView(tableView, estimatedHeightForRowAt: indexPath)
+ func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+ return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
+ }
+
+ func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
+ return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
+ }
+
+ func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
+ return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
+ }
+
+ func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
+ aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
}
}
@@ -278,19 +328,6 @@ extension NotificationViewController {
break
}
}
-
- func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
- guard let diffableDataSource = viewModel.diffableDataSource else { return }
- guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
- switch item {
- case .bottomLoader:
- if !tableView.isDragging, !tableView.isDecelerating {
- viewModel.loadOldestStateMachine.enter(NotificationViewModel.LoadOldestState.Loading.self)
- }
- default:
- break
- }
- }
}
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
@@ -388,6 +425,7 @@ extension NotificationViewController: ScrollViewContainer {
}
}
+// MARK: - LoadMoreConfigurableTableViewContainer
extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
typealias LoadingState = NotificationViewModel.LoadOldestState.Loading
@@ -395,6 +433,24 @@ extension NotificationViewController: LoadMoreConfigurableTableViewContainer {
var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.loadOldestStateMachine }
}
+// MARK: - AVPlayerViewControllerDelegate
+extension NotificationViewController: AVPlayerViewControllerDelegate {
+ func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
+ }
+
+ func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
+ }
+}
+
+// MARK: - statusTableViewCellDelegate
+extension NotificationViewController: StatusTableViewCellDelegate {
+ var playerViewControllerDelegate: AVPlayerViewControllerDelegate? {
+ return self
+ }
+}
+
extension NotificationViewController {
enum CategorySwitch: String, CaseIterable {
@@ -452,9 +508,9 @@ extension NotificationViewController {
switch category {
case .showEverything:
- viewModel.selectedIndex.value = .EveryThing
+ viewModel.selectedIndex.value = .everyThing
case .showMentions:
- viewModel.selectedIndex.value = .Mentions
+ viewModel.selectedIndex.value = .mentions
}
}
diff --git a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift
index 6a542bf2c..6c7a70e43 100644
--- a/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift
+++ b/Mastodon/Scene/Notification/NotificationViewModel+Diffable.swift
@@ -14,14 +14,16 @@ import MastodonSDK
extension NotificationViewModel {
func setupDiffableDataSource(
for tableView: UITableView,
+ dependency: NeedsDependency,
delegate: NotificationTableViewCellDelegate,
- dependency: NeedsDependency
+ statusTableViewCellDelegate: StatusTableViewCellDelegate
) {
diffableDataSource = NotificationSection.tableViewDiffableDataSource(
for: tableView,
+ dependency: dependency,
managedObjectContext: fetchedResultsController.managedObjectContext,
delegate: delegate,
- dependency: dependency
+ statusTableViewCellDelegate: statusTableViewCellDelegate
)
var snapshot = NSDiffableDataSourceSnapshot()
@@ -81,11 +83,23 @@ extension NotificationViewModel: NSFetchedResultsControllerDelegate {
}
var newSnapshot = NSDiffableDataSourceSnapshot()
newSnapshot.appendSections([.main])
- let items: [NotificationItem] = notifications.map { notification in
- let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
- return NotificationItem.notification(objectID: notification.objectID, attribute: attribute)
+
+ let segment = self.selectedIndex.value
+ switch segment {
+ case .everyThing:
+ let items: [NotificationItem] = notifications.map { notification in
+ let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
+ return NotificationItem.notification(objectID: notification.objectID, attribute: attribute)
+ }
+ newSnapshot.appendItems(items, toSection: .main)
+ case .mentions:
+ let items: [NotificationItem] = notifications.map { notification in
+ let attribute: Item.StatusAttribute = oldSnapshotAttributeDict[notification.objectID] ?? Item.StatusAttribute()
+ return NotificationItem.notificationStatus(objectID: notification.objectID, attribute: attribute)
+ }
+ newSnapshot.appendItems(items, toSection: .main)
}
- newSnapshot.appendItems(items, toSection: .main)
+
if !notifications.isEmpty, self.noMoreNotification.value == false {
newSnapshot.appendItems([.bottomLoader], toSection: .main)
}
diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift
index 9567d6cbb..bf2c03174 100644
--- a/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift
+++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadOldestState.swift
@@ -92,13 +92,13 @@ extension NotificationViewModel.LoadOldestState {
} receiveValue: { [weak viewModel] response in
guard let viewModel = viewModel else { return }
switch viewModel.selectedIndex.value {
- case .EveryThing:
+ case .everyThing:
if response.value.isEmpty {
stateMachine.enter(NoMore.self)
} else {
stateMachine.enter(Idle.self)
}
- case .Mentions:
+ case .mentions:
viewModel.noMoreNotification.value = response.value.isEmpty
let list = response.value.filter { $0.type == Mastodon.Entity.Notification.NotificationType.mention }
if list.isEmpty {
diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift
index 712380917..98b7deec3 100644
--- a/Mastodon/Scene/Notification/NotificationViewModel.swift
+++ b/Mastodon/Scene/Notification/NotificationViewModel.swift
@@ -23,13 +23,13 @@ final class NotificationViewModel: NSObject {
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
let viewDidLoad = PassthroughSubject()
- let selectedIndex = CurrentValueSubject(.EveryThing)
+ let selectedIndex = CurrentValueSubject(.everyThing)
let noMoreNotification = CurrentValueSubject(false)
let activeMastodonAuthenticationBox: CurrentValueSubject
let fetchedResultsController: NSFetchedResultsController!
let notificationPredicate = CurrentValueSubject(nil)
- let cellFrameCache = NSCache()
+ let cellFrameCache = NSCache()
var needsScrollToTopAfterDataSourceUpdate = false
let dataSourceDidUpdated = PassthroughSubject()
@@ -161,7 +161,7 @@ final class NotificationViewModel: NSObject {
extension NotificationViewModel {
enum NotificationSegment: Int {
- case EveryThing
- case Mentions
+ case everyThing
+ case mentions
}
}
diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
index b86c46745..8428aaa79 100644
--- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
+++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift
@@ -312,7 +312,7 @@ extension MastodonRegisterViewController {
view.addGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer.addTarget(self, action: #selector(tapGestureRecognizerHandler))
- // stackview
+ // stackView
stackView.axis = .vertical
stackView.distribution = .fill
stackView.spacing = 40
@@ -370,7 +370,7 @@ extension MastodonRegisterViewController {
scrollView.frameLayoutGuide.widthAnchor.constraint(equalTo: scrollView.contentLayoutGuide.widthAnchor),
])
- // stackview
+ // stackView
scrollView.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@@ -802,23 +802,48 @@ extension MastodonRegisterViewController {
let password = viewModel.password.value
let locale: String = {
- let fallbackLanguageCode = Locale.current.languageCode ?? "en"
+ guard let url = Bundle.main.url(forResource: "local-codes", withExtension: "json"),
+ let data = try? Data(contentsOf: url),
+ let localCode = try? JSONDecoder().decode(MastodonLocalCode.self, from: data)
+ else {
+ assertionFailure()
+ return "en"
+ }
+ let fallbackLanguageCode: String = {
+ let code = Locale.current.languageCode ?? "en"
+ guard localCode[code] != nil else { return "en" }
+ return code
+ }()
+
+ // pick device preferred language
guard let identifier = Locale.preferredLanguages.first else {
return fallbackLanguageCode
}
+ // prepare languageCode and validate then return fallback if needs
let local = Locale(identifier: identifier)
- guard let languageCode = local.languageCode else {
+ guard let languageCode = local.languageCode,
+ localCode[languageCode] != nil
+ else {
return fallbackLanguageCode
}
- switch languageCode {
- case "zh":
- // Check Simplified Chinese / Traditional Chinese
- // https://github.com/gunchleoc/mastodon/blob/ed6153b8f24d3a8f5a124cc95683bd1f20aec882/app/helpers/settings_helper.rb
- guard let regionCode = local.regionCode else { return languageCode }
- return "zh" + "-" + regionCode
- default:
+ // prepare extendCode and validate then return fallback if needs
+ let extendCodes: [String] = {
+ let locales = Locale.preferredLanguages.map { Locale(identifier: $0) }
+ return locales.compactMap { locale in
+ guard let languageCode = locale.languageCode,
+ let regionCode = locale.regionCode
+ else { return nil }
+ return languageCode + "-" + regionCode
+ }
+ }()
+ let _firstMatchExtendCode = extendCodes.first { code in
+ localCode[code] != nil
+ }
+ guard let firstMatchExtendCode = _firstMatchExtendCode else {
return languageCode
}
+ return firstMatchExtendCode
+
}()
let query = Mastodon.API.Account.RegisterQuery(
reason: viewModel.reason.value,
@@ -828,6 +853,8 @@ extension MastodonRegisterViewController {
agreement: true, // user confirmed in the server rules scene
locale: locale
)
+
+ var retryCount = 0
// register without show server rules
context.apiService.accountRegister(
@@ -835,6 +862,32 @@ extension MastodonRegisterViewController {
query: query,
authorization: viewModel.applicationAuthorization
)
+ .tryCatch { [weak self] error -> AnyPublisher, Error> in
+ guard let self = self else { throw error }
+ guard let error = self.viewModel.error.value as? Mastodon.API.Error,
+ case let .generic(errorEntity) = error.mastodonError,
+ errorEntity.error == "Validation failed: Locale is not included in the list"
+ else {
+ throw error
+ }
+ guard retryCount == 0 else {
+ throw error
+ }
+ let retryQuery = Mastodon.API.Account.RegisterQuery(
+ reason: query.reason,
+ username: query.username,
+ email: query.email,
+ password: query.password,
+ agreement: query.agreement,
+ locale: self.viewModel.instance.languages?.first ?? "en"
+ )
+ retryCount += 1
+ return self.context.apiService.accountRegister(
+ domain: self.viewModel.domain,
+ query: retryQuery,
+ authorization: self.viewModel.applicationAuthorization
+ )
+ }
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
diff --git a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift
index 4a4d04bf6..17c4699ec 100644
--- a/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift
+++ b/Mastodon/Scene/Onboarding/Share/OnboardingViewControllerAppearance.swift
@@ -80,8 +80,8 @@ 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
+ return 20
+// let shortEdgeWidth = min(UIScreen.main.bounds.height, UIScreen.main.bounds.width)
+// return shortEdgeWidth * 0.17 // magic
}
}
diff --git a/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift b/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift
index 024fb205d..6f18afc94 100644
--- a/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift
+++ b/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift
@@ -96,6 +96,13 @@ extension WizardCardView {
path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY), radius: radius, startAngle: -.pi / 2, endAngle: -.pi, clockwise: false)
path.addLine(to: CGPoint(x: rect.minX - radius, y: rect.maxY + radius + WizardCardView.bubbleArrowHeight))
path.close()
+ case .allCorners: // no arrow
+ path.move(to: CGPoint(x: rect.maxX, y: rect.maxY + radius))
+ path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.maxY), radius: radius, startAngle: .pi / 2, endAngle: .pi, clockwise: true)
+ path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY), radius: radius, startAngle: .pi, endAngle: .pi / 2 * 3, clockwise: true)
+ path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.minY), radius: radius, startAngle: .pi / 2 * 3, endAngle: .pi * 2, clockwise: true)
+ path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius, startAngle: .pi * 2, endAngle: .pi / 2 * 5, clockwise: true)
+ path.close()
default:
assertionFailure("FIXME")
}
diff --git a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
index a2a266f9d..bf33ea13d 100644
--- a/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
+++ b/Mastodon/Scene/Onboarding/Welcome/WelcomeViewController.swift
@@ -15,6 +15,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set()
+ var observations = Set()
private(set) lazy var viewModel = WelcomeViewModel(context: context)
let welcomeIllustrationView = WelcomeIllustrationView()
@@ -23,7 +24,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
private(set) lazy var dismissBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(WelcomeViewController.dismissBarButtonItemDidPressed(_:)))
private(set) lazy var logoImageView: UIImageView = {
- let image = view.traitCollection.userInterfaceIdiom == .phone ? Asset.Scene.Welcome.mastodonLogo.image : Asset.Scene.Welcome.mastodonLogoBlackLarge.image
+ let image = Asset.Scene.Welcome.mastodonLogo.image
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
@@ -40,15 +41,15 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
return label
}()
- private(set) lazy var signUpButton: PrimaryActionButton = {
+ private(set) lazy var signUpButton: PrimaryActionButton = {
let button = PrimaryActionButton()
button.adjustsBackgroundImageWhenUserInterfaceStyleChanges = false
button.setTitle(L10n.Common.Controls.Actions.signUp, for: .normal)
- let backgroundImageColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? .white : Asset.Colors.brandBlue.color
- let backgroundImageHighlightedColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor(white: 0.8, alpha: 1.0) : Asset.Colors.brandBlueDarken20.color
+ let backgroundImageColor: UIColor = .white
+ let backgroundImageHighlightedColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
button.setBackgroundImage(.placeholder(color: backgroundImageColor), for: .normal)
button.setBackgroundImage(.placeholder(color: backgroundImageHighlightedColor), for: .highlighted)
- let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? Asset.Colors.brandBlue.color : UIColor.white
+ let titleColor: UIColor = Asset.Colors.brandBlue.color
button.setTitleColor(titleColor, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
@@ -58,7 +59,7 @@ final class WelcomeViewController: UIViewController, NeedsDependency {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
button.setTitle(L10n.Common.Controls.Actions.signIn, for: .normal)
- let titleColor: UIColor = traitCollection.userInterfaceIdiom == .phone ? UIColor.white.withAlphaComponent(0.8) : Asset.Colors.brandBlue.color
+ let titleColor: UIColor = UIColor.white.withAlphaComponent(0.8)
button.setTitleColor(titleColor, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
return button
@@ -75,6 +76,8 @@ extension WelcomeViewController {
override func viewDidLoad() {
super.viewDidLoad()
+ // preferredContentSize = CGSize(width: 547, height: 678)
+
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .never
view.overrideUserInterfaceStyle = .light
@@ -106,16 +109,31 @@ extension WelcomeViewController {
self.navigationItem.leftBarButtonItem = needsShowDismissEntry ? self.dismissBarButtonItem : nil
}
.store(in: &disposeBag)
+
+ view.observe(\.frame, options: [.initial, .new]) { [weak self] view, _ in
+ guard let self = self else { return }
+ switch view.traitCollection.userInterfaceIdiom {
+ case .phone:
+ break
+ default:
+ self.welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.isHidden = view.frame.height < 800
+ }
+ }
+ .store(in: &observations)
}
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
- // shift illustration down for non-notch phone
var overlap: CGFloat = 5
+ // shift illustration down for non-notch phone
if view.safeAreaInsets.bottom == 0 {
overlap += 56
}
+ // shift illustration down for iPad modal
+ if UIDevice.current.userInterfaceIdiom != .phone {
+ overlap += 20
+ }
welcomeIllustrationViewBottomAnchorLayoutConstraint?.constant = overlap
}
@@ -137,85 +155,80 @@ extension WelcomeViewController {
}
// set illustration for phone
- if traitCollection.userInterfaceIdiom == .phone {
- guard welcomeIllustrationView.superview == nil else {
- return
- }
-
- welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
- welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 5)
-
- view.addSubview(welcomeIllustrationView)
- NSLayoutConstraint.activate([
- view.leftAnchor.constraint(equalTo: welcomeIllustrationView.leftAnchor, constant: 15),
- welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 15),
- welcomeIllustrationViewBottomAnchorLayoutConstraint!
- ])
-
- welcomeIllustrationView.cloudBaseImageView.addMotionEffect(
- UIInterpolatingMotionEffect.motionEffect(minX: -5, maxX: 5, minY: -5, maxY: 5)
- )
- welcomeIllustrationView.rightHillImageView.addMotionEffect(
- UIInterpolatingMotionEffect.motionEffect(minX: -15, maxX: 25, minY: -10, maxY: 10)
- )
- welcomeIllustrationView.leftHillImageView.addMotionEffect(
- UIInterpolatingMotionEffect.motionEffect(minX: -25, maxX: 15, minY: -15, maxY: 15)
- )
- welcomeIllustrationView.centerHillImageView.addMotionEffect(
- UIInterpolatingMotionEffect.motionEffect(minX: -14, maxX: 14, minY: -5, maxY: 25)
- )
-
- let topPaddingView = UIView()
- topPaddingView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(topPaddingView)
- NSLayoutConstraint.activate([
- topPaddingView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor),
- topPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
- topPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
- ])
- welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(welcomeIllustrationView.elephantOnAirplaneWithContrailImageView)
- NSLayoutConstraint.activate([
- view.leftAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.leftAnchor, constant: 12), // add 12pt bleeding
- welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
- // make a little bit large
- welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.84),
- welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.heightAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor, multiplier: 105.0/318.0),
- ])
- let bottomPaddingView = UIView()
- bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(bottomPaddingView)
- NSLayoutConstraint.activate([
- bottomPaddingView.topAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.bottomAnchor),
- bottomPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
- bottomPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
- bottomPaddingView.bottomAnchor.constraint(equalTo: view.centerYAnchor),
- bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 4),
- ])
-
- welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.addMotionEffect(
- UIInterpolatingMotionEffect.motionEffect(minX: -20, maxX: 12, minY: -20, maxY: 12) // maxX should not larger then the bleeding (12pt)
- )
-
- view.bringSubviewToFront(logoImageView)
- view.bringSubviewToFront(sloganLabel)
- }
-
- // set slogan for non-phone
- if traitCollection.userInterfaceIdiom != .phone {
- guard sloganLabel.superview == nil else {
- return
- }
- view.addSubview(sloganLabel)
- NSLayoutConstraint.activate([
- sloganLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 16),
- view.readableContentGuide.trailingAnchor.constraint(equalTo: sloganLabel.trailingAnchor, constant: 16),
- sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
- ])
+ guard welcomeIllustrationView.superview == nil else {
+ return
}
- view.bringSubviewToFront(sloganLabel)
+ welcomeIllustrationView.translatesAutoresizingMaskIntoConstraints = false
+ welcomeIllustrationViewBottomAnchorLayoutConstraint = welcomeIllustrationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 5)
+
+ view.addSubview(welcomeIllustrationView)
+ NSLayoutConstraint.activate([
+ view.leftAnchor.constraint(equalTo: welcomeIllustrationView.leftAnchor, constant: 15),
+ welcomeIllustrationView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 15),
+ welcomeIllustrationViewBottomAnchorLayoutConstraint!
+ ])
+
+ welcomeIllustrationView.cloudBaseImageView.addMotionEffect(
+ UIInterpolatingMotionEffect.motionEffect(minX: -5, maxX: 5, minY: -5, maxY: 5)
+ )
+ welcomeIllustrationView.rightHillImageView.addMotionEffect(
+ UIInterpolatingMotionEffect.motionEffect(minX: -15, maxX: 25, minY: -10, maxY: 10)
+ )
+ welcomeIllustrationView.leftHillImageView.addMotionEffect(
+ UIInterpolatingMotionEffect.motionEffect(minX: -25, maxX: 15, minY: -15, maxY: 15)
+ )
+ welcomeIllustrationView.centerHillImageView.addMotionEffect(
+ UIInterpolatingMotionEffect.motionEffect(minX: -14, maxX: 14, minY: -5, maxY: 25)
+ )
+
+ let topPaddingView = UIView()
+ topPaddingView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(topPaddingView)
+ NSLayoutConstraint.activate([
+ topPaddingView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor),
+ topPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
+ topPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
+ ])
+ welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(welcomeIllustrationView.elephantOnAirplaneWithContrailImageView)
+ NSLayoutConstraint.activate([
+ view.leftAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.leftAnchor, constant: 12), // add 12pt bleeding
+ welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.topAnchor.constraint(equalTo: topPaddingView.bottomAnchor),
+ // make a little bit large
+ welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.84),
+ welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.heightAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.widthAnchor, multiplier: 105.0/318.0),
+ ])
+ let bottomPaddingView = UIView()
+ bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(bottomPaddingView)
+ NSLayoutConstraint.activate([
+ bottomPaddingView.topAnchor.constraint(equalTo: welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.bottomAnchor),
+ bottomPaddingView.leadingAnchor.constraint(equalTo: logoImageView.leadingAnchor),
+ bottomPaddingView.trailingAnchor.constraint(equalTo: logoImageView.trailingAnchor),
+ bottomPaddingView.bottomAnchor.constraint(equalTo: view.centerYAnchor),
+ bottomPaddingView.heightAnchor.constraint(equalTo: topPaddingView.heightAnchor, multiplier: 4),
+ ])
+
+ welcomeIllustrationView.elephantOnAirplaneWithContrailImageView.addMotionEffect(
+ UIInterpolatingMotionEffect.motionEffect(minX: -20, maxX: 12, minY: -20, maxY: 12) // maxX should not larger then the bleeding (12pt)
+ )
+
view.bringSubviewToFront(logoImageView)
+ view.bringSubviewToFront(sloganLabel)
+
+ // set slogan for non-phone
+// if traitCollection.userInterfaceIdiom != .phone {
+// guard sloganLabel.superview == nil else {
+// return
+// }
+// view.addSubview(sloganLabel)
+// NSLayoutConstraint.activate([
+// sloganLabel.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor, constant: 16),
+// view.readableContentGuide.trailingAnchor.constraint(equalTo: sloganLabel.trailingAnchor, constant: 16),
+// sloganLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 168),
+// ])
+// }
}
}
@@ -261,12 +274,12 @@ extension WelcomeViewController: UIAdaptivePresentationControllerDelegate {
// make underneath view controller alive to fix layout issue due to view life cycle
return .fullScreen
default:
- switch traitCollection.horizontalSizeClass {
- case .regular:
- return .pageSheet
- default:
- return .fullScreen
- }
+ return .formSheet
+// switch traitCollection.horizontalSizeClass {
+// case .regular:
+// default:
+// return .fullScreen
+// }
}
}
diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift
index 627ed7772..25e102846 100644
--- a/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift
+++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController+Provider.swift
@@ -37,7 +37,8 @@ extension FollowerListViewController: UserProvider {
let managedObjectContext = self.viewModel.userFetchedResultsController.fetchedResultsController.managedObjectContext
switch item {
- case .follower(let objectID):
+ case .follower(let objectID),
+ .following(let objectID):
managedObjectContext.perform {
let user = managedObjectContext.object(with: objectID) as? MastodonUser
promise(.success(user))
diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift
index 428448666..97e62ea8d 100644
--- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift
+++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift
@@ -7,11 +7,10 @@
import os.log
import UIKit
-import AVKit
import GameplayKit
import Combine
-final class FollowerListViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
+final class FollowerListViewController: UIViewController, NeedsDependency {
var disposeBag = Set()
@@ -19,9 +18,7 @@ final class FollowerListViewController: UIViewController, NeedsDependency, Media
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))
diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift
index 90b9cb311..fc9f31779 100644
--- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift
+++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+Diffable.swift
@@ -18,16 +18,19 @@ extension FollowerListViewModel {
managedObjectContext: userFetchedResultsController.fetchedResultsController.managedObjectContext
)
+ // workaround to append loader wrong animation issue
// 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)
+ if #available(iOS 15.0, *) {
+ diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
+ } else {
+ // Fallback on earlier versions
+ diffableDataSource?.apply(snapshot, animatingDifferences: false)
+ }
- userFetchedResultsController.objectIDs.removeDuplicates()
+ userFetchedResultsController.objectIDs
.receive(on: DispatchQueue.main)
.sink { [weak self] objectIDs in
guard let self = self else { return }
@@ -45,7 +48,12 @@ extension FollowerListViewModel {
case is State.Idle, is State.Loading, is State.Fail:
snapshot.appendItems([.bottomLoader], toSection: .main)
case is State.NoMore:
- break
+ guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value,
+ let userID = self.userID.value,
+ userID != activeMastodonAuthenticationBox.userID
+ else { break }
+ let text = L10n.Scene.Follower.footer
+ snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
default:
break
}
diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift
index b012a59bb..43e532673 100644
--- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift
+++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift
@@ -155,7 +155,7 @@ extension FollowerListViewModel.State {
let maxID = response.link?.maxID
- if hasNewAppend, maxID != nil {
+ if hasNewAppend && maxID != nil {
stateMachine.enter(Idle.self)
} else {
stateMachine.enter(NoMore.self)
@@ -179,18 +179,6 @@ extension FollowerListViewModel.State {
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/Following/FollowingListViewController+Provider.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController+Provider.swift
new file mode 100644
index 000000000..aaeb52328
--- /dev/null
+++ b/Mastodon/Scene/Profile/Following/FollowingListViewController+Provider.swift
@@ -0,0 +1,51 @@
+//
+// FollowingListViewController+Provider.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+
+extension FollowingListViewController: 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),
+ .following(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/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift
new file mode 100644
index 000000000..35691b82d
--- /dev/null
+++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift
@@ -0,0 +1,108 @@
+//
+// FollowingListViewController.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import os.log
+import UIKit
+import GameplayKit
+import Combine
+
+final class FollowingListViewController: UIViewController, NeedsDependency {
+
+ var disposeBag = Set()
+
+ weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
+ weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+
+ var viewModel: FollowingListViewModel!
+
+ 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 FollowingListViewController {
+
+ 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(FollowingListViewModel.State.Reloading.self)
+ }
+ .store(in: &disposeBag)
+ }
+
+}
+
+// MARK: - LoadMoreConfigurableTableViewContainer
+extension FollowingListViewController: LoadMoreConfigurableTableViewContainer {
+ typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
+ typealias LoadingState = FollowingListViewModel.State.Loading
+ var loadMoreConfigurableTableView: UITableView { tableView }
+ var loadMoreConfigurableStateMachine: GKStateMachine { viewModel.stateMachine }
+}
+
+// MARK: - UIScrollViewDelegate
+extension FollowingListViewController {
+ func scrollViewDidScroll(_ scrollView: UIScrollView) {
+ handleScrollViewDidScroll(scrollView)
+ }
+}
+
+
+// MARK: - UITableViewDelegate
+extension FollowingListViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ handleTableView(tableView, didSelectRowAt: indexPath)
+ }
+}
+
+// MARK: - UserTableViewCellDelegate
+extension FollowingListViewController: UserTableViewCellDelegate { }
diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift
new file mode 100644
index 000000000..dc6f1f6fd
--- /dev/null
+++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+Diffable.swift
@@ -0,0 +1,66 @@
+//
+// FollowingListViewModel+Diffable.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import UIKit
+
+extension FollowingListViewModel {
+ func setupDiffableDataSource(
+ for tableView: UITableView,
+ dependency: NeedsDependency
+ ) {
+ diffableDataSource = UserSection.tableViewDiffableDataSource(
+ for: tableView,
+ dependency: dependency,
+ managedObjectContext: userFetchedResultsController.fetchedResultsController.managedObjectContext
+ )
+
+ // workaround to append loader wrong animation issue
+ // set empty section to make update animation top-to-bottom style
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+ snapshot.appendItems([.bottomLoader], toSection: .main)
+ if #available(iOS 15.0, *) {
+ diffableDataSource?.applySnapshotUsingReloadData(snapshot, completion: nil)
+ } else {
+ // Fallback on earlier versions
+ diffableDataSource?.apply(snapshot, animatingDifferences: false)
+ }
+
+ userFetchedResultsController.objectIDs
+ .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.following(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:
+ guard let activeMastodonAuthenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value,
+ let userID = self.userID.value,
+ userID != activeMastodonAuthenticationBox.userID
+ else { break }
+ let text = L10n.Scene.Following.footer
+ snapshot.appendItems([.bottomHeader(text: text)], toSection: .main)
+ default:
+ break
+ }
+ }
+
+ diffableDataSource.apply(snapshot)
+ }
+ .store(in: &disposeBag)
+ }
+}
diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift
new file mode 100644
index 000000000..0ec3d6262
--- /dev/null
+++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift
@@ -0,0 +1,184 @@
+//
+// FollowingListViewModel+State.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import os.log
+import Foundation
+import GameplayKit
+import MastodonSDK
+
+extension FollowingListViewModel {
+ class State: GKState {
+ weak var viewModel: FollowingListViewModel?
+
+ init(viewModel: FollowingListViewModel) {
+ 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 FollowingListViewModel.State {
+ class Initial: FollowingListViewModel.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: FollowingListViewModel.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: FollowingListViewModel.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: FollowingListViewModel.State {
+ override func isValidNextState(_ stateClass: AnyClass) -> Bool {
+ switch stateClass {
+ case is Reloading.Type, is Loading.Type:
+ return true
+ default:
+ return false
+ }
+ }
+ }
+
+ class Loading: FollowingListViewModel.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.following(
+ 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: FollowingListViewModel.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)
+ }
+ }
+}
diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift
new file mode 100644
index 000000000..0677f6cb4
--- /dev/null
+++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel.swift
@@ -0,0 +1,53 @@
+//
+// FollowingListViewModel.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import Foundation
+import Combine
+import Combine
+import CoreData
+import CoreDataStack
+import GameplayKit
+import MastodonSDK
+
+final class FollowingListViewModel {
+
+ 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/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index 04d582315..5ff71ba99 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -1001,8 +1001,19 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
transition: .show
)
case .following:
- // TODO:
- break
+ guard let domain = viewModel.domain.value,
+ let userID = viewModel.userID.value
+ else { return }
+ let followingListViewModel = FollowingListViewModel(
+ context: context,
+ domain: domain,
+ userID: userID
+ )
+ coordinator.present(
+ scene: .following(viewModel: followingListViewModel),
+ from: self,
+ transition: .show
+ )
}
}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
index 42e9376cf..4bee3b8af 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
@@ -72,6 +72,16 @@ extension UserTimelineViewController {
statusTableViewCellDelegate: self
)
+ // setup batch fetch
+ viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
+ viewModel.listBatchFetchViewModel.shouldFetch
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ self.viewModel.stateMachine.enter(UserTimelineViewModel.State.Loading.self)
+ }
+ .store(in: &disposeBag)
+
// trigger user timeline loading
Publishers.CombineLatest(
viewModel.domain.removeDuplicates().eraseToAnyPublisher(),
@@ -104,11 +114,11 @@ extension UserTimelineViewController {
extension UserTimelineViewController: StatusTableViewControllerAspect { }
// MARK: - UIScrollViewDelegate
-extension UserTimelineViewController {
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- aspectScrollViewDidScroll(scrollView)
- }
-}
+//extension UserTimelineViewController {
+// func scrollViewDidScroll(_ scrollView: UIScrollView) {
+// aspectScrollViewDidScroll(scrollView)
+// }
+//}
// MARK: - TableViewCellHeightCacheableContainer
extension UserTimelineViewController: TableViewCellHeightCacheableContainer {
@@ -186,13 +196,13 @@ extension UserTimelineViewController: ScrollViewContainer {
}
// MARK: - LoadMoreConfigurableTableViewContainer
-extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
- typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
- typealias LoadingState = UserTimelineViewModel.State.Loading
-
- var loadMoreConfigurableTableView: UITableView { return tableView }
- var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
-}
+//extension UserTimelineViewController: LoadMoreConfigurableTableViewContainer {
+// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
+// typealias LoadingState = UserTimelineViewModel.State.Loading
+//
+// var loadMoreConfigurableTableView: UITable``````View { return tableView }
+// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
+//}
extension UserTimelineViewController {
override var keyCommands: [UIKeyCommand]? {
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
index 42edafb0f..5bf520d6d 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
@@ -23,6 +23,7 @@ final class UserTimelineViewModel {
let userID: CurrentValueSubject
let queryFilter: CurrentValueSubject
let statusFetchedResultsController: StatusFetchedResultsController
+ let listBatchFetchViewModel = ListBatchFetchViewModel()
var cellFrameCache = NSCache()
let isBlocking = CurrentValueSubject(false)
diff --git a/Mastodon/Scene/Report/ReportViewController.swift b/Mastodon/Scene/Report/ReportViewController.swift
index 6a7161c91..b97424cb5 100644
--- a/Mastodon/Scene/Report/ReportViewController.swift
+++ b/Mastodon/Scene/Report/ReportViewController.swift
@@ -51,7 +51,7 @@ class ReportViewController: UIViewController, NeedsDependency {
return view
}()
- lazy var stackview: UIStackView = {
+ lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.alignment = .fill
@@ -122,19 +122,19 @@ class ReportViewController: UIViewController, NeedsDependency {
setupNavigation()
- stackview.addArrangedSubview(header)
- stackview.addArrangedSubview(contentView)
- stackview.addArrangedSubview(footer)
- stackview.addArrangedSubview(bottomSpacing)
+ stackView.addArrangedSubview(header)
+ stackView.addArrangedSubview(contentView)
+ stackView.addArrangedSubview(footer)
+ stackView.addArrangedSubview(bottomSpacing)
contentView.addSubview(tableView)
- view.addSubview(stackview)
+ view.addSubview(stackView)
NSLayoutConstraint.activate([
- stackview.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
- stackview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- stackview.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- stackview.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo: contentView.topAnchor),
tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
@@ -273,8 +273,8 @@ class ReportViewController: UIViewController, NeedsDependency {
navigationItem.titleView = titleView
if let user = beReportedUser {
do {
- let mastodonConent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
- let metaContent = try MastodonMetaContent.convert(document: mastodonConent)
+ let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
+ let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
titleView.update(titleMetaContent: metaContent, subtitle: nil)
} catch {
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
diff --git a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift
index ff566a248..0880c479a 100644
--- a/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift
+++ b/Mastodon/Scene/Report/ReportedStatusTableviewCell.swift
@@ -19,6 +19,7 @@ final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
static let bottomPaddingHeight: CGFloat = 10
weak var dependency: ReportViewController?
+ private var _disposeBag = Set()
var disposeBag = Set()
var observations = Set()
@@ -98,7 +99,7 @@ extension ReportedStatusTableViewCell {
guard let self = self else { return }
self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
- .store(in: &disposeBag)
+ .store(in: &_disposeBag)
checkbox.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(checkbox)
diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift
index 850b1429f..8ca597872 100644
--- a/Mastodon/Scene/Root/ContentSplitViewController.swift
+++ b/Mastodon/Scene/Root/ContentSplitViewController.swift
@@ -101,7 +101,8 @@ extension ContentSplitViewController: SidebarViewControllerDelegate {
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)
+ // content width needs > 300 to make checkmark display
+ accountListViewController.preferredContentSize = CGSize(width: 375, height: 400)
}
}
diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift
deleted file mode 100644
index b69a6b786..000000000
--- a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift
+++ /dev/null
@@ -1,137 +0,0 @@
-//
-// MainTabBarController+Wizard.swift
-// Mastodon
-//
-// Created by Cirno MainasuK on 2021-9-15.
-//
-
-import os.log
-import UIKit
-
-protocol WizardDelegate: AnyObject {
- func spotlight(item: MainTabBarController.Wizard.Item) -> UIBezierPath
- func layoutWizardCard(_ wizard: MainTabBarController.Wizard, item: MainTabBarController.Wizard.Item)
-}
-
-extension MainTabBarController {
- class Wizard {
-
- let logger = Logger(subsystem: "Wizard", category: "UI")
-
- weak var delegate: WizardDelegate?
-
- private(set) var items: [Item]
-
- let backgroundView: UIView = {
- let view = UIView()
- view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
- return view
- }()
-
- init() {
- var items: [Item] = []
- if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
- items.append(.multipleAccountSwitch)
- }
- self.items = items
-
- let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
- backgroundTapGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.Wizard.backgroundTapGestureRecognizerHandler(_:)))
- backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
- }
- }
-}
-
-extension MainTabBarController.Wizard {
- enum Item {
- case multipleAccountSwitch
-
- var title: String {
- return L10n.Scene.Wizard.newInMastodon
- }
-
- var description: String {
- switch self {
- case .multipleAccountSwitch:
- return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
- }
- }
-
- func markAsRead() {
- switch self {
- case .multipleAccountSwitch:
- UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
- }
- }
- }
-}
-
-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)
- NSLayoutConstraint.activate([
- backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
- backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- ])
- }
-
- func consume() {
- guard !items.isEmpty else {
- backgroundView.removeFromSuperview()
- return
- }
- let item = items.removeFirst()
- perform(item: item)
- }
-
- private func perform(item: Item) {
- guard let delegate = delegate else {
- assertionFailure()
- return
- }
-
- // prepare for reuse
- prepareForReuse()
-
- // set wizard item read
- item.markAsRead()
-
- // add spotlight
- let spotlight = delegate.spotlight(item: item)
- let maskLayer = CAShapeLayer()
- let path = UIBezierPath(rect: backgroundView.bounds)
- path.append(spotlight)
- maskLayer.fillRule = .evenOdd
- maskLayer.path = path.cgPath
- backgroundView.layer.mask = maskLayer
-
- // layout wizard card
- delegate.layoutWizardCard(self, item: item)
- }
-
- private func prepareForReuse() {
- backgroundView.subviews.forEach { subview in
- subview.removeFromSuperview()
- }
- backgroundView.mask = nil
- backgroundView.layer.mask = nil
- }
-
-}
-
-extension MainTabBarController.Wizard {
- @objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
- logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-
- consume()
- }
-}
diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
index d34c85531..4b803bc49 100644
--- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
+++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
@@ -21,8 +21,6 @@ class MainTabBarController: UITabBarController {
static let avatarButtonSize = CGSize(width: 25, height: 25)
let avatarButton = CircleAvatarButton()
-
- let wizard = Wizard()
var currentTab = CurrentValueSubject(.home)
@@ -108,6 +106,8 @@ class MainTabBarController: UITabBarController {
var _viewControllers: [UIViewController] = []
+ private(set) var isReadyForWizardAvatarButton = false
+
init(context: AppContext, coordinator: SceneCoordinator) {
self.context = context
self.coordinator = coordinator
@@ -247,14 +247,27 @@ extension MainTabBarController {
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
}
.store(in: &disposeBag)
-
- wizard.delegate = self
- wizard.setup(in: view)
let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer()
tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:)))
tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer)
+ context.authenticationService.activeMastodonAuthenticationBox
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] authenticationBox in
+ guard let self = self else { return }
+ self.isReadyForWizardAvatarButton = authenticationBox != nil
+ }
+ .store(in: &disposeBag)
+
+ currentTab
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] tab in
+ guard let self = self else { return }
+ self.updateAvatarButtonAppearance()
+ }
+ .store(in: &disposeBag)
+
updateTabBarDisplay()
#if DEBUG
@@ -262,16 +275,11 @@ extension MainTabBarController {
#endif
}
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
- wizard.consume()
- }
-
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateTabBarDisplay()
+ updateAvatarButtonAppearance()
}
}
@@ -343,6 +351,11 @@ extension MainTabBarController {
self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
self.avatarButton.isUserInteractionEnabled = false
}
+
+ private func updateAvatarButtonAppearance() {
+ avatarButton.borderColor = currentTab.value == .me ? .label : .systemFill
+ avatarButton.setNeedsLayout()
+ }
}
extension MainTabBarController {
@@ -351,6 +364,10 @@ extension MainTabBarController {
return viewController(of: NotificationViewController.self)
}
+ var searchViewController: SearchViewController? {
+ return viewController(of: SearchViewController.self)
+ }
+
}
// MARK: - UITabBarControllerDelegate
@@ -373,51 +390,57 @@ extension MainTabBarController: UITabBarControllerDelegate {
}
}
-// MARK: - WizardDataSource
-extension MainTabBarController: WizardDelegate {
- func spotlight(item: Wizard.Item) -> UIBezierPath {
+// MARK: - WizardViewControllerDelegate
+extension MainTabBarController: WizardViewControllerDelegate {
+ func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool {
switch item {
case .multipleAccountSwitch:
- guard let avatarButtonFrameInView = avatarButtonFrameInView() else {
- return UIBezierPath()
- }
- return UIBezierPath(ovalIn: avatarButtonFrameInView)
-
+ return isReadyForWizardAvatarButton
}
}
- func layoutWizardCard(_ wizard: MainTabBarController.Wizard, item: Wizard.Item) {
+ func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath {
switch item {
case .multipleAccountSwitch:
- guard let avatarButtonFrameInView = avatarButtonFrameInView() else {
+ guard let avatarButtonFrameInView = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
+ return UIBezierPath()
+ }
+ return UIBezierPath(ovalIn: avatarButtonFrameInView)
+ }
+ }
+
+ func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item) {
+ switch item {
+ case .multipleAccountSwitch:
+ guard let avatarButtonFrameInView = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
return
}
let anchorView = UIView()
anchorView.frame = avatarButtonFrameInView
- wizard.backgroundView.addSubview(anchorView)
+ wizardViewController.backgroundView.addSubview(anchorView)
let wizardCardView = WizardCardView()
wizardCardView.arrowRectCorner = view.traitCollection.layoutDirection == .leftToRight ? .bottomRight : .bottomLeft
wizardCardView.titleLabel.text = item.title
wizardCardView.descriptionLabel.text = item.description
-
+
wizardCardView.translatesAutoresizingMaskIntoConstraints = false
- wizard.backgroundView.addSubview(wizardCardView)
+ wizardViewController.backgroundView.addSubview(wizardCardView)
NSLayoutConstraint.activate([
anchorView.topAnchor.constraint(equalTo: wizardCardView.bottomAnchor, constant: 13), // 13pt spacing
wizardCardView.trailingAnchor.constraint(equalTo: anchorView.centerXAnchor),
- wizardCardView.widthAnchor.constraint(equalTo: wizard.backgroundView.widthAnchor, multiplier: 2.0/3.0).priority(.required - 1),
+ wizardCardView.widthAnchor.constraint(equalTo: wizardViewController.view.widthAnchor, multiplier: 2.0/3.0).priority(.required - 1),
])
wizardCardView.setContentHuggingPriority(.defaultLow, for: .vertical)
}
}
- private func avatarButtonFrameInView() -> CGRect? {
+ private func avatarButtonFrameInWizardView(wizardView: UIView) -> CGRect? {
guard let superview = avatarButton.superview else {
assertionFailure()
return nil
}
- return superview.convert(avatarButton.frame, to: view)
+ return superview.convert(avatarButton.frame, to: wizardView)
}
}
diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift
index 7c03287f1..e9d7549bd 100644
--- a/Mastodon/Scene/Root/RootSplitViewController.swift
+++ b/Mastodon/Scene/Root/RootSplitViewController.swift
@@ -19,6 +19,8 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+ private var isPrimaryDisplay = false
+
private(set) lazy var contentSplitViewController: ContentSplitViewController = {
let contentSplitViewController = ContentSplitViewController()
contentSplitViewController.context = context
@@ -79,13 +81,6 @@ extension RootSplitViewController {
super.viewDidLoad()
updateBehavior(size: view.frame.size)
- contentSplitViewController.$currentSupplementaryTab
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- self.updateBehavior(size: self.view.frame.size)
- }
- .store(in: &disposeBag)
setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
@@ -97,6 +92,12 @@ extension RootSplitViewController {
.store(in: &disposeBag)
}
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+
+ updateBehavior(size: view.frame.size)
+ }
+
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
@@ -108,16 +109,30 @@ extension RootSplitViewController {
}
}
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+
+ setupBackground(theme: ThemeService.shared.currentTheme.value)
+ }
+
private func updateBehavior(size: CGSize) {
- switch contentSplitViewController.currentSupplementaryTab {
- case .search:
+ if size.width > 960 {
+ show(.primary)
+ isPrimaryDisplay = true
+
+ } else {
hide(.primary)
+ isPrimaryDisplay = false
+ }
+
+ switch (contentSplitViewController.currentSupplementaryTab, isPrimaryDisplay) {
+ case (.search, true):
+ // needs switch to other tab when primary display
+ // use FIFO queue save tab history
+ contentSplitViewController.currentSupplementaryTab = .home
default:
- if size.width > 960 {
- show(.primary)
- } else {
- hide(.primary)
- }
+ // do nothing
+ break
}
}
@@ -140,7 +155,11 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
return
}
switch tab {
- case .search:
+ case .search:
+ guard isPrimaryDisplay else {
+ // only control search tab behavior when primary display
+ fallthrough
+ }
guard let navigationController = searchViewController.navigationController else { return }
if navigationController.viewControllers.count == 1 {
searchViewController.searchBarTapPublisher.send()
@@ -165,7 +184,7 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
// MARK: - UISplitViewControllerDelegate
extension RootSplitViewController: UISplitViewControllerDelegate {
- private static func transform(from: UITabBarController, to: UITabBarController) {
+ private static func transform(from: UITabBarController, to: UITabBarController) {
let sourceNavigationControllers = from.viewControllers ?? []
let targetNavigationControllers = to.viewControllers ?? []
@@ -181,6 +200,11 @@ extension RootSplitViewController: UISplitViewControllerDelegate {
to.selectedIndex = from.selectedIndex
}
+ private static func transform(from: UINavigationController, to: UINavigationController) {
+ let viewControllers = from.popToRootViewController(animated: false) ?? []
+ to.viewControllers.append(contentsOf: viewControllers)
+ }
+
// .regular to .compact
func splitViewController(
_ svc: UISplitViewController,
@@ -223,3 +247,90 @@ extension RootSplitViewController: UISplitViewControllerDelegate {
}
}
+
+// MARK: - WizardViewControllerDelegate
+extension RootSplitViewController: WizardViewControllerDelegate {
+
+ func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool {
+ guard traitCollection.horizontalSizeClass != .compact else {
+ return compactMainTabBarViewController.readyToLayoutItem(wizardViewController, item: item)
+ }
+
+ switch item {
+ case .multipleAccountSwitch:
+ return contentSplitViewController.sidebarViewController.viewModel.isReadyForWizardAvatarButton
+ }
+ }
+
+
+ func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath {
+ guard traitCollection.horizontalSizeClass != .compact else {
+ return compactMainTabBarViewController.layoutSpotlight(wizardViewController, item: item)
+ }
+
+ switch item {
+ case .multipleAccountSwitch:
+ guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view)
+ else {
+ assertionFailure()
+ return UIBezierPath()
+ }
+ return UIBezierPath(ovalIn: frame)
+ }
+ }
+
+ func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item) {
+ guard traitCollection.horizontalSizeClass != .compact else {
+ return compactMainTabBarViewController.layoutWizardCard(wizardViewController, item: item)
+ }
+
+ guard let frame = avatarButtonFrameInWizardView(wizardView: wizardViewController.view) else {
+ return
+ }
+
+ let anchorView = UIView()
+ anchorView.frame = frame
+ wizardViewController.backgroundView.addSubview(anchorView)
+
+ let wizardCardView = WizardCardView()
+ wizardCardView.arrowRectCorner = .allCorners // no arrow
+ wizardCardView.titleLabel.text = item.title
+ wizardCardView.descriptionLabel.text = item.description
+
+ wizardCardView.translatesAutoresizingMaskIntoConstraints = false
+ wizardViewController.backgroundView.addSubview(wizardCardView)
+ NSLayoutConstraint.activate([
+ wizardCardView.centerYAnchor.constraint(equalTo: anchorView.centerYAnchor),
+ wizardCardView.leadingAnchor.constraint(equalTo: anchorView.trailingAnchor, constant: 20), // 20pt spacing
+ wizardCardView.widthAnchor.constraint(equalToConstant: 320),
+ ])
+ wizardCardView.setContentHuggingPriority(.defaultLow, for: .vertical)
+ }
+
+ private func avatarButtonFrameInWizardView(wizardView: UIView) -> CGRect? {
+ guard let diffableDataSource = contentSplitViewController.sidebarViewController.viewModel.diffableDataSource,
+ let indexPath = diffableDataSource.indexPath(for: .tab(.me)),
+ let cell = contentSplitViewController.sidebarViewController.collectionView.cellForItem(at: indexPath) as? SidebarListCollectionViewCell,
+ let contentView = cell._contentView,
+ let frame = sourceViewFrameInTargetView(
+ sourceView: contentView.avatarButton,
+ targetView: wizardView
+ )
+ else {
+ assertionFailure()
+ return nil
+ }
+ return frame
+ }
+
+ private func sourceViewFrameInTargetView(
+ sourceView: UIView,
+ targetView: UIView
+ ) -> CGRect? {
+ guard let superview = sourceView.superview else {
+ assertionFailure()
+ return nil
+ }
+ return superview.convert(sourceView.frame, to: targetView)
+ }
+}
diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift
index 7958c5080..b5f67e769 100644
--- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift
+++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift
@@ -125,9 +125,16 @@ extension SidebarViewController {
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
+
+ let contentHeight = secondaryCollectionView.contentSize.height
+ guard contentHeight > 0 else { return }
+ self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): secondaryCollectionView contentSize: \(secondaryCollectionView.contentSize.debugDescription)")
+
+ let currentFrameHeight = secondaryCollectionView.frame.height
+ guard currentFrameHeight < contentHeight else { return }
+
+ self.secondaryCollectionViewHeightLayoutConstraint.constant = contentHeight
+ self.collectionView.contentInset.bottom = contentHeight
}
.store(in: &observations)
@@ -146,7 +153,6 @@ extension SidebarViewController {
coordinator.animate { context in
self.collectionView.collectionViewLayout.invalidateLayout()
-// // do nothing
} completion: { [weak self] context in
// guard let self = self else { return }
}
diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift
index 83abf4e6b..37b46932b 100644
--- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift
+++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift
@@ -18,31 +18,36 @@ final class SidebarViewModel {
// input
let context: AppContext
- let searchHistoryFetchedResultController: SearchHistoryFetchedResultController
-
+ @Published private var isSidebarDataSourceReady = false
+ @Published private var isAvatarButtonDataReady = false
+
// output
var diffableDataSource: UICollectionViewDiffableDataSource?
var secondaryDiffableDataSource: UICollectionViewDiffableDataSource?
+ @Published private(set) var isReadyForWizardAvatarButton = false
let activeMastodonAuthenticationObjectID = CurrentValueSubject(nil)
init(context: AppContext) {
self.context = context
- self.searchHistoryFetchedResultController = SearchHistoryFetchedResultController(managedObjectContext: context.managedObjectContext)
+
+ Publishers.CombineLatest(
+ $isSidebarDataSourceReady,
+ $isAvatarButtonDataReady
+ )
+ .map { $0 && $1 }
+ .assign(to: &$isReadyForWizardAvatarButton)
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] authentication in
guard let self = self else { return }
- // bind search history
- self.searchHistoryFetchedResultController.domain.value = authentication?.domain
- self.searchHistoryFetchedResultController.userID.value = authentication?.userID
// bind objectID
self.activeMastodonAuthenticationObjectID.value = authentication?.objectID
+
+ self.isAvatarButtonDataReady = authentication != nil
}
.store(in: &disposeBag)
-
- try? searchHistoryFetchedResultController.fetchedResultsController.performFetch()
}
}
@@ -140,7 +145,7 @@ extension SidebarViewModel {
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
case .compose:
let item = SidebarListContentView.Item(
- title: "Compose", // TODO: update i18n
+ title: L10n.Common.Controls.Actions.compose,
image: UIImage(systemName: "square.and.pencil")!,
imageURL: nil
)
@@ -170,8 +175,12 @@ extension SidebarViewModel {
.setting,
]
sectionSnapshot.append(items, to: nil)
- _diffableDataSource.apply(sectionSnapshot, to: .main)
-
+ // animatingDifferences must to be `true`
+ // otherwise the UI layout will infinity loop
+ _diffableDataSource.apply(sectionSnapshot, to: .main, animatingDifferences: true) { [weak self] in
+ guard let self = self else { return }
+ self.isSidebarDataSourceReady = true
+ }
// secondary
let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource(collectionView: secondaryCollectionView) { collectionView, indexPath, item in
@@ -181,7 +190,7 @@ extension SidebarViewModel {
}
let item = SidebarListContentView.Item(
- title: "Compose", // FIXME:
+ title: L10n.Common.Controls.Actions.compose,
image: UIImage(systemName: "square.and.pencil")!,
imageURL: nil
)
diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift
index d85d3a8be..d6ae40e17 100644
--- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift
+++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift
@@ -18,7 +18,7 @@ final class SidebarListContentView: UIView, UIContentView {
let avatarButton: CircleAvatarButton = {
let button = CircleAvatarButton()
button.borderWidth = 2
- button.borderColor = UIColor.label.cgColor
+ button.borderColor = UIColor.label
return button
}()
diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift
index 2056c5dcd..6a1bb3ddf 100644
--- a/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift
+++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift
@@ -11,7 +11,8 @@ final class SidebarListHeaderView: UICollectionReusableView {
let imageView: UIImageView = {
let imageView = UIImageView()
- imageView.image = Asset.Scene.Sidebar.logo.image
+ imageView.image = Asset.Scene.Sidebar.logo.image.withRenderingMode(.alwaysTemplate)
+ imageView.tintColor = .label
return imageView
}()
diff --git a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift
index 3734bc8a4..3a20788b5 100644
--- a/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift
+++ b/Mastodon/Scene/Search/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift
@@ -27,6 +27,7 @@ class SearchRecommendTagsCollectionViewCell: UICollectionViewCell {
let label = UILabel()
label.textColor = .white
label.font = .preferredFont(forTextStyle: .body)
+ label.numberOfLines = 2
return label
}()
diff --git a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
index b401e7955..486a3b48a 100644
--- a/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
+++ b/Mastodon/Scene/Search/SearchDetail/SearchDetailViewController.swift
@@ -245,7 +245,9 @@ extension SearchDetailViewController {
searchBar.becomeFirstResponder()
} else {
searchController.isActive = true
- searchController.searchBar.becomeFirstResponder()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.33) {
+ self.searchController.searchBar.becomeFirstResponder()
+ }
}
}
diff --git a/Mastodon/Scene/Share/View/Button/AvatarButton.swift b/Mastodon/Scene/Share/View/Button/AvatarButton.swift
index 9bc87a053..6249ea373 100644
--- a/Mastodon/Scene/Share/View/Button/AvatarButton.swift
+++ b/Mastodon/Scene/Share/View/Button/AvatarButton.swift
@@ -38,6 +38,18 @@ class AvatarButton: UIControl {
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ updateAppearance()
+ }
+
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+
+ updateAppearance()
+ }
func updateAppearance() {
avatarImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0
diff --git a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift
index 0bc2aeefd..74591dda9 100644
--- a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift
+++ b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift
@@ -9,15 +9,18 @@ import UIKit
final class CircleAvatarButton: AvatarButton {
- var borderColor: CGColor = UIColor.systemFill.cgColor
+ @Published var needsHighlighted = false
+
+ var borderColor: UIColor = UIColor.systemFill
var borderWidth: CGFloat = 1.0
- override func layoutSubviews() {
- super.layoutSubviews()
+ override func updateAppearance() {
+ super.updateAppearance()
layer.masksToBounds = true
layer.cornerRadius = frame.width * 0.5
- layer.borderColor = borderColor
+ layer.borderColor = borderColor.cgColor
layer.borderWidth = borderWidth
}
+
}
diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift
index 957764fa7..62eb3d6b0 100644
--- a/Mastodon/Scene/Share/View/Content/StatusView.swift
+++ b/Mastodon/Scene/Share/View/Content/StatusView.swift
@@ -203,6 +203,9 @@ final class StatusView: UIView {
return actionToolbarContainer
}()
+ // set display when needs bottom padding
+ let actionToolbarPlaceholderPaddingView = UIView()
+
let contentMetaText: MetaText = {
let metaText = MetaText()
metaText.textView.backgroundColor = .clear
@@ -451,6 +454,13 @@ extension StatusView {
containerStackView.sendSubviewToBack(actionToolbarContainer)
actionToolbarContainer.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
actionToolbarContainer.setContentHuggingPriority(.required - 1, for: .vertical)
+
+ actionToolbarPlaceholderPaddingView.translatesAutoresizingMaskIntoConstraints = false
+ containerStackView.addArrangedSubview(actionToolbarPlaceholderPaddingView)
+ NSLayoutConstraint.activate([
+ actionToolbarPlaceholderPaddingView.heightAnchor.constraint(equalToConstant: 12).priority(.required - 1),
+ ])
+ actionToolbarPlaceholderPaddingView.isHidden = true
headerContainerView.isHidden = true
statusMosaicImageViewContainer.isHidden = true
diff --git a/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift b/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift
new file mode 100644
index 000000000..78eaf6ae3
--- /dev/null
+++ b/Mastodon/Scene/Share/ViewModel/ListBatchFetchViewModel.swift
@@ -0,0 +1,68 @@
+//
+// ListBatchFetchViewModel.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-10.
+//
+
+import UIKit
+import Combine
+
+// ref: Texture.ASBatchFetchingDelegate
+final class ListBatchFetchViewModel {
+
+ var disposeBag = Set()
+
+ // timer running on `common` mode
+ let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
+ .autoconnect()
+ .share()
+ .eraseToAnyPublisher()
+
+ // input
+ private(set) weak var scrollView: UIScrollView?
+ let hasMore = CurrentValueSubject(true)
+
+ // output
+ let shouldFetch = PassthroughSubject()
+
+ init() {
+ Publishers.CombineLatest(
+ hasMore,
+ timerPublisher
+ )
+ .sink { [weak self] hasMore, _ in
+ guard let self = self else { return }
+ guard hasMore else { return }
+ guard let scrollView = self.scrollView else { return }
+
+ // skip trigger if user interacting
+ if scrollView.isDragging || scrollView.isTracking { return }
+
+ // send fetch request
+ if scrollView.contentSize.height < scrollView.frame.height {
+ self.shouldFetch.send()
+ } else {
+ let frame = scrollView.frame
+ let contentOffset = scrollView.contentOffset
+ let contentSize = scrollView.contentSize
+
+ let visibleBottomY = contentOffset.y + frame.height
+ let offset = 2 * frame.height
+ let fetchThrottleOffsetY = contentSize.height - offset
+
+ if visibleBottomY > fetchThrottleOffsetY {
+ self.shouldFetch.send()
+ }
+ }
+ }
+ .store(in: &disposeBag)
+ }
+
+}
+
+extension ListBatchFetchViewModel {
+ func setup(scrollView: UIScrollView) {
+ self.scrollView = scrollView
+ }
+}
diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
index 9fbbbd888..ec4ac35ad 100644
--- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
+++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift
@@ -184,9 +184,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath
let maskLayerToFinalRect: CGRect? = {
guard case .mosaic = transitionItem.source else { return nil }
- guard let tabBarController = toVC.tabBarController, let tabBarSuperView = tabBarController.tabBar.superview else { return nil }
- let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
var rect = maskLayerToRect ?? transitionMaskView.frame
+ // clip tabBar when bar visible
+ guard let tabBarController = toVC.tabBarController,
+ !tabBarController.tabBar.isHidden,
+ let tabBarSuperView = tabBarController.tabBar.superview
+ else { return rect }
+ let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
let offset = rect.maxY - tabBarFrameInWindow.minY
guard offset > 0 else { return rect }
rect.size.height -= offset
@@ -473,9 +477,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning {
let maskLayerToFinalRect: CGRect? = {
guard case .mosaic = transitionItem.source else { return nil }
- guard let tabBarController = toVC.tabBarController, let tabBarSuperView = tabBarController.tabBar.superview else { return nil }
- let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
var rect = maskLayerToRect ?? transitionMaskView.frame
+ // clip rect bottom when tabBar visible
+ guard let tabBarController = toVC.tabBarController,
+ !tabBarController.tabBar.isHidden,
+ let tabBarSuperView = tabBarController.tabBar.superview
+ else { return rect }
+ let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil)
let offset = rect.maxY - tabBarFrameInWindow.minY
guard offset > 0 else { return rect }
rect.size.height -= offset
diff --git a/Mastodon/Scene/Wizard/WizardViewController.swift b/Mastodon/Scene/Wizard/WizardViewController.swift
new file mode 100644
index 000000000..2678c712d
--- /dev/null
+++ b/Mastodon/Scene/Wizard/WizardViewController.swift
@@ -0,0 +1,211 @@
+//
+// WizardViewController.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import os.log
+import UIKit
+import Combine
+
+protocol WizardViewControllerDelegate: AnyObject {
+ func readyToLayoutItem(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> Bool
+ func layoutSpotlight(_ wizardViewController: WizardViewController, item: WizardViewController.Item) -> UIBezierPath
+ func layoutWizardCard(_ wizardViewController: WizardViewController, item: WizardViewController.Item)
+}
+
+class WizardViewController: UIViewController {
+
+ let logger = Logger(subsystem: "Wizard", category: "UI")
+
+ var disposeBag = Set()
+ weak var delegate: WizardViewControllerDelegate?
+
+ private(set) var items: [Item] = {
+ var items: [Item] = []
+ if !UserDefaults.shared.didShowMultipleAccountSwitchWizard {
+ items.append(.multipleAccountSwitch)
+ }
+ return items
+ }()
+
+ let pendingItem = CurrentValueSubject- (nil)
+ let currentItem = CurrentValueSubject
- (nil)
+
+ let backgroundView: UIView = {
+ let view = UIView()
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
+ return view
+ }()
+
+ deinit {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension WizardViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setup()
+
+ let backgroundTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
+ backgroundTapGestureRecognizer.addTarget(self, action: #selector(WizardViewController.backgroundTapGestureRecognizerHandler(_:)))
+ backgroundView.addGestureRecognizer(backgroundTapGestureRecognizer)
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+
+ // Create a timer to consume pending item
+ Timer.publish(every: 0.5, on: .main, in: .default)
+ .autoconnect()
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ guard self.pendingItem.value != nil else { return }
+ self.consume()
+ }
+ .store(in: &disposeBag)
+
+ consume()
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+
+ invalidLayoutForCurrentItem()
+ }
+
+ override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+ super.viewWillTransition(to: size, with: coordinator)
+
+ coordinator.animate { context in
+
+ } completion: { [weak self] context in
+ guard let self = self else { return }
+ self.invalidLayoutForCurrentItem()
+ }
+
+ }
+
+}
+
+extension WizardViewController {
+ enum Item {
+ case multipleAccountSwitch
+
+ var title: String {
+ return L10n.Scene.Wizard.newInMastodon
+ }
+
+ var description: String {
+ switch self {
+ case .multipleAccountSwitch:
+ return L10n.Scene.Wizard.multipleAccountSwitchIntroDescription
+ }
+ }
+
+ func markAsRead() {
+ switch self {
+ case .multipleAccountSwitch:
+ UserDefaults.shared.didShowMultipleAccountSwitchWizard = true
+ }
+ }
+ }
+}
+
+extension WizardViewController {
+
+ func setup() {
+ assert(delegate != nil, "need set delegate before use")
+
+ guard !items.isEmpty else { return }
+
+ backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ backgroundView.frame = view.bounds
+ view.addSubview(backgroundView)
+ }
+
+ func destroy() {
+ view.removeFromSuperview()
+ }
+
+ func consume() {
+ guard !items.isEmpty else {
+ destroy()
+ return
+ }
+
+ guard let first = items.first else { return }
+ guard delegate?.readyToLayoutItem(self, item: first) == true else {
+ pendingItem.value = first
+ return
+ }
+ pendingItem.value = nil
+ currentItem.value = nil
+
+ let item = items.removeFirst()
+ perform(item: item)
+ }
+
+ private func perform(item: Item) {
+ guard let delegate = delegate else {
+ assertionFailure()
+ return
+ }
+
+ // prepare for reuse
+ prepareForReuse()
+
+ // set wizard item read
+ item.markAsRead()
+
+ // add spotlight
+ let spotlight = delegate.layoutSpotlight(self, item: item)
+ let maskLayer = CAShapeLayer()
+ // expand rect to make sure view always fill the screen when device rotate
+ let expandRect: CGRect = {
+ var rect = backgroundView.bounds
+ rect.size.width *= 2
+ rect.size.height *= 2
+ return rect
+ }()
+ let path = UIBezierPath(rect: expandRect)
+ path.append(spotlight)
+ maskLayer.fillRule = .evenOdd
+ maskLayer.path = path.cgPath
+ backgroundView.layer.mask = maskLayer
+
+ // layout wizard card
+ delegate.layoutWizardCard(self, item: item)
+
+ currentItem.value = item
+ }
+
+ private func prepareForReuse() {
+ backgroundView.subviews.forEach { subview in
+ subview.removeFromSuperview()
+ }
+ backgroundView.mask = nil
+ backgroundView.layer.mask = nil
+ }
+
+ private func invalidLayoutForCurrentItem() {
+ if let item = currentItem.value {
+ perform(item: item)
+ }
+ }
+
+}
+
+extension WizardViewController {
+ @objc private func backgroundTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
+
+ consume()
+ }
+}
diff --git a/Mastodon/Service/APIService/APIService+Follower.swift b/Mastodon/Service/APIService/APIService+Follower.swift
index db29a0a29..f75d2420d 100644
--- a/Mastodon/Service/APIService/APIService+Follower.swift
+++ b/Mastodon/Service/APIService/APIService+Follower.swift
@@ -23,10 +23,15 @@ extension APIService {
let authorization = authorizationBox.userAuthorization
let requestMastodonUserID = authorizationBox.userID
+ let query = Mastodon.API.Account.FollowerQuery(
+ maxID: maxID,
+ limit: nil
+ )
return Mastodon.API.Account.followers(
session: session,
domain: domain,
userID: userID,
+ query: query,
authorization: authorization
)
.flatMap { response -> AnyPublisher, Error> in
diff --git a/Mastodon/Service/APIService/APIService+Following.swift b/Mastodon/Service/APIService/APIService+Following.swift
new file mode 100644
index 000000000..8f477d6ec
--- /dev/null
+++ b/Mastodon/Service/APIService/APIService+Following.swift
@@ -0,0 +1,70 @@
+//
+// APIService+Following.swift
+// Mastodon
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import UIKit
+import Combine
+import CoreData
+import CoreDataStack
+import CommonOSLog
+import MastodonSDK
+
+extension APIService {
+
+ func following(
+ userID: Mastodon.Entity.Account.ID,
+ maxID: String?,
+ authorizationBox: MastodonAuthenticationBox
+ ) -> AnyPublisher, Error> {
+ let domain = authorizationBox.domain
+ let authorization = authorizationBox.userAuthorization
+ let requestMastodonUserID = authorizationBox.userID
+
+ let query = Mastodon.API.Account.FollowingQuery(
+ maxID: maxID,
+ limit: nil
+ )
+ return Mastodon.API.Account.following(
+ session: session,
+ domain: domain,
+ userID: userID,
+ query: query,
+ 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/StatusPublishService.swift b/Mastodon/Service/StatusPublishService.swift
index ed894f933..f5c4cb2dd 100644
--- a/Mastodon/Service/StatusPublishService.swift
+++ b/Mastodon/Service/StatusPublishService.swift
@@ -12,6 +12,7 @@ import Combine
import CoreData
import CoreDataStack
import MastodonSDK
+import UIKit
final class StatusPublishService {
@@ -72,7 +73,6 @@ extension StatusPublishService {
self.viewModels.value = viewModels
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: composeViewModel removed", ((#file as NSString).lastPathComponent), #line, #function)
-
}
}
diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift
index 87d16241a..e2cb7c41b 100644
--- a/Mastodon/Supporting Files/AppDelegate.swift
+++ b/Mastodon/Supporting Files/AppDelegate.swift
@@ -36,6 +36,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications()
+ // increase app process count
+ var count = UserDefaults.shared.processCompletedCount
+ count += 1 // Int64. could ignore overflow here
+ UserDefaults.shared.processCompletedCount = count
+
#if ASDK && DEBUG
// PerformanceMonitor.shared().start()
// ASDisplayNode.shouldShowRangeDebugOverlay = true
diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist
index ed243297d..8ac3d165b 100644
--- a/MastodonIntent/Info.plist
+++ b/MastodonIntent/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
NSExtension
NSExtensionAttributes
diff --git a/MastodonIntent/fr.lproj/Intents.strings b/MastodonIntent/fr.lproj/Intents.strings
index 628f794c9..f4fec3000 100644
--- a/MastodonIntent/fr.lproj/Intents.strings
+++ b/MastodonIntent/fr.lproj/Intents.strings
@@ -6,7 +6,7 @@
"HZSGTr" = "Quel contenu à publier ?";
-"HdGikU" = "Posting failed";
+"HdGikU" = "Échec lors de la publication";
"KDNTJ4" = "Raison de l’échec";
@@ -40,11 +40,11 @@
"gfePDu" = "Échec lors de la publication. ${failureReason}";
-"k7dbKQ" = "Post was sent successfully.";
+"k7dbKQ" = "Message publié avec succès.";
-"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?";
+"oGiqmY-dYQ5NN" = "Juste pour confirmer, vous vouliez « Public » ?";
-"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?";
+"oGiqmY-ehFLjY" = "Juste pour confirmer, vous vouliez bien diffuser vers « abonné·e·s uniquement » ?";
"rM6dvp" = "URL";
diff --git a/MastodonIntent/ku-TR.lproj/Intents.strings b/MastodonIntent/ku-TR.lproj/Intents.strings
index 3e1c69fc3..13a86e0c0 100644
--- a/MastodonIntent/ku-TR.lproj/Intents.strings
+++ b/MastodonIntent/ku-TR.lproj/Intents.strings
@@ -24,15 +24,15 @@
"Zo4jgJ" = "Xuyaniya şandiyê";
-"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Giştî' ne.";
+"apSxMG-dYQ5NN" = "Vebijarkên ${count} hene ku li gorî 'Gelemperî' ne.";
-"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopandin' hene.";
+"apSxMG-ehFLjY" = "Vebijarkên ${count} hene ku li gorî 'Tenê Şopaneran' hene.";
-"ayoYEb-dYQ5NN" = "${content}, Giştî";
+"ayoYEb-dYQ5NN" = "${content}, Gelemperî";
"ayoYEb-ehFLjY" = "${content}, Tenê şopînêr";
-"dUyuGg" = "Li ser Mastodon bişînin";
+"dUyuGg" = "Di Mastodon de biweşîne";
"dYQ5NN" = "Gelemperî";
@@ -42,10 +42,10 @@
"k7dbKQ" = "Şandî bi serkeftî hate şandin.";
-"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, we 'Giştî' dixwest?";
+"oGiqmY-dYQ5NN" = "Tenê ji bo pejirandinê, te 'Gelemperî' dixwest?";
-"oGiqmY-ehFLjY" = "Tenê ji bo piştrastkirinê, we 'Tenê Şopdarên' dixwest?";
+"oGiqmY-ehFLjY" = "Tenê ji bo pejirandinê, te 'Tenê Şopîner' dixwest?";
"rM6dvp" = "Girêdan";
-"ryJLwG" = "Bi serkeftî hat şandin. ";
+"ryJLwG" = "Şandî bi serkeftî hate şandin. ";
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift
index b09a5f07b..a900a1c12 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Followers.swift
@@ -35,11 +35,12 @@ extension Mastodon.API.Account {
session: URLSession,
domain: String,
userID: Mastodon.Entity.Account.ID,
+ query: FollowerQuery,
authorization: Mastodon.API.OAuth.Authorization
) -> AnyPublisher, Error> {
let request = Mastodon.API.get(
url: followersEndpointURL(domain: domain, userID: userID),
- query: nil,
+ query: query,
authorization: authorization
)
return session.dataTaskPublisher(for: request)
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Following.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Following.swift
new file mode 100644
index 000000000..c992c7584
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Following.swift
@@ -0,0 +1,82 @@
+//
+// Mastodon+API+Account+Following.swift
+//
+//
+// Created by Cirno MainasuK on 2021-11-2.
+//
+
+import Foundation
+import Combine
+
+extension Mastodon.API.Account {
+
+ static func followingEndpointURL(domain: String, userID: Mastodon.Entity.Account.ID) -> URL {
+ return Mastodon.API.endpointURL(domain: domain)
+ .appendingPathComponent("accounts")
+ .appendingPathComponent(userID)
+ .appendingPathComponent("following")
+ }
+
+ /// Following
+ ///
+ /// Accounts which the given account is following, 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 following(
+ session: URLSession,
+ domain: String,
+ userID: Mastodon.Entity.Account.ID,
+ query: FollowingQuery,
+ authorization: Mastodon.API.OAuth.Authorization
+ ) -> AnyPublisher, Error> {
+ let request = Mastodon.API.get(
+ url: followingEndpointURL(domain: domain, userID: userID),
+ query: query,
+ 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 FollowingQuery: 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/MastodonTests/Info.plist b/MastodonTests/Info.plist
index 16c084cec..9fe845c60 100644
--- a/MastodonTests/Info.plist
+++ b/MastodonTests/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist
index 16c084cec..9fe845c60 100644
--- a/MastodonUITests/Info.plist
+++ b/MastodonUITests/Info.plist
@@ -17,6 +17,6 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist
index 89e562534..8e14f3a2a 100644
--- a/NotificationService/Info.plist
+++ b/NotificationService/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
NSExtension
NSExtensionPointIdentifier
diff --git a/README.md b/README.md
index 08c87e558..e1686b2e8 100644
--- a/README.md
+++ b/README.md
@@ -85,4 +85,5 @@ The app is compatible with [toot-relay](https://github.com/DagAgren/toot-relay)
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
## License
-This project is released under the [GPL-3 License](./LICENSE).
+
+This project is released under the [GPL-3 License](./LICENSE). It is also dual-licensed to Apple for the purposes of publishing the app on the App Store. For this reason, any contributors are required to sign a Contributor License Agreement.
diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist
index 79ba82cef..1b3025474 100644
--- a/ShareActionExtension/Info.plist
+++ b/ShareActionExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
1.2.0
CFBundleVersion
- 82
+ 88
NSExtension
NSExtensionAttributes