diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict
index 051bb50ef..cd97825f4 100644
--- a/Localization/Localizable.stringsdict
+++ b/Localization/Localizable.stringsdict
@@ -68,6 +68,28 @@
%ld characters
+ a11y.plural.count.characters_left
+
+ NSStringLocalizedFormatKey
+ %#@character_count@ left
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
plural.count.followed_by_and_mutual
NSStringLocalizedFormatKey
diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings
index 6877490ba..6f29830a1 100644
--- a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings
+++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.strings
@@ -1,51 +1,51 @@
-"16wxgf" = "Post on Mastodon";
+"16wxgf" = "Příspěvek na Mastodon";
-"751xkl" = "Text Content";
+"751xkl" = "Textový obsah";
-"CsR7G2" = "Post on Mastodon";
+"CsR7G2" = "Příspěvek na Mastodon";
-"HZSGTr" = "What content to post?";
+"HZSGTr" = "Jaký obsah se má přidat?";
-"HdGikU" = "Posting failed";
+"HdGikU" = "Odeslání se nezdařilo";
-"KDNTJ4" = "Failure Reason";
+"KDNTJ4" = "Důvod selhání";
-"RHxKOw" = "Send Post with text content";
+"RHxKOw" = "Odeslat příspěvek s textovým obsahem";
-"RxSqsb" = "Post";
+"RxSqsb" = "Příspěvek";
-"WCIR3D" = "Post ${content} on Mastodon";
+"WCIR3D" = "Zveřejnit ${content} na Mastodon";
-"ZKJSNu" = "Post";
+"ZKJSNu" = "Příspěvek";
"ZS1XaK" = "${content}";
-"ZbSjzC" = "Visibility";
+"ZbSjzC" = "Viditelnost";
-"Zo4jgJ" = "Post Visibility";
+"Zo4jgJ" = "Viditelnost příspěvku";
-"apSxMG-dYQ5NN" = "There are ${count} options matching ‘Public’.";
+"apSxMG-dYQ5NN" = "Existuje ${count} možností odpovídajících 'Veřejný'.";
-"apSxMG-ehFLjY" = "There are ${count} options matching ‘Followers Only’.";
+"apSxMG-ehFLjY" = "Existuje ${count} možností, které odpovídají „jen sledujícím“.";
-"ayoYEb-dYQ5NN" = "${content}, Public";
+"ayoYEb-dYQ5NN" = "${content}, veřejné";
-"ayoYEb-ehFLjY" = "${content}, Followers Only";
+"ayoYEb-ehFLjY" = "${content}, pouze sledující";
-"dUyuGg" = "Post on Mastodon";
+"dUyuGg" = "Příspěvek na Mastodon";
-"dYQ5NN" = "Public";
+"dYQ5NN" = "Veřejný";
-"ehFLjY" = "Followers Only";
+"ehFLjY" = "Pouze sledující";
-"gfePDu" = "Posting failed. ${failureReason}";
+"gfePDu" = "Odeslání se nezdařilo. ${failureReason}";
-"k7dbKQ" = "Post was sent successfully.";
+"k7dbKQ" = "Příspěvek byl úspěšně odeslán.";
-"oGiqmY-dYQ5NN" = "Just to confirm, you wanted ‘Public’?";
+"oGiqmY-dYQ5NN" = "Jen pro kontrolu, chtěli jste „Veřejný“?";
-"oGiqmY-ehFLjY" = "Just to confirm, you wanted ‘Followers Only’?";
+"oGiqmY-ehFLjY" = "Jen pro kontrolu, chtěli jste „Pouze sledující“?";
"rM6dvp" = "URL";
-"ryJLwG" = "Post was sent successfully. ";
+"ryJLwG" = "Příspěvek byl úspěšně odeslán. ";
diff --git a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict
index a739f778f..deea8db12 100644
--- a/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict
+++ b/Localization/StringsConvertor/Intents/input/cs.lproj/Intents.stringsdict
@@ -5,7 +5,7 @@
There are ${count} options matching ‘${content}’. - 2
NSStringLocalizedFormatKey
- There are %#@count_option@ matching ‘${content}’.
+ Existuje %#@count_option@ odpovídající „${content}“.
count_option
NSStringFormatSpecTypeKey
diff --git a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings
index 6877490ba..4d4e426c6 100644
--- a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings
+++ b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.strings
@@ -1,26 +1,26 @@
-"16wxgf" = "Post on Mastodon";
+"16wxgf" = "Postar no Mastodon";
-"751xkl" = "Text Content";
+"751xkl" = "Conteúdo do texto";
-"CsR7G2" = "Post on Mastodon";
+"CsR7G2" = "Postar no Mastodon";
"HZSGTr" = "What content to post?";
"HdGikU" = "Posting failed";
-"KDNTJ4" = "Failure Reason";
+"KDNTJ4" = "Motivo da falha";
-"RHxKOw" = "Send Post with text content";
+"RHxKOw" = "Enviar postagem com conteúdo de texto";
-"RxSqsb" = "Post";
+"RxSqsb" = "Postagem";
-"WCIR3D" = "Post ${content} on Mastodon";
+"WCIR3D" = "Postar ${content} no Mastodon";
-"ZKJSNu" = "Post";
+"ZKJSNu" = "Postar";
"ZS1XaK" = "${content}";
-"ZbSjzC" = "Visibility";
+"ZbSjzC" = "Visibilidade";
"Zo4jgJ" = "Post Visibility";
diff --git a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict
index 18422c772..a48559b4a 100644
--- a/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict
+++ b/Localization/StringsConvertor/Intents/input/pt-BR.lproj/Intents.stringsdict
@@ -5,7 +5,7 @@
There are ${count} options matching ‘${content}’. - 2
NSStringLocalizedFormatKey
- There are %#@count_option@ matching ‘${content}’.
+ Existem %#@count_option@ opções correspondentes a ‘${content}’.
count_option
NSStringFormatSpecTypeKey
@@ -13,15 +13,15 @@
NSStringFormatValueTypeKey
%ld
one
- 1 option
+ 1 opção
other
- %ld options
+ %ld opções
There are ${count} options matching ‘${visibility}’.
NSStringLocalizedFormatKey
- There are %#@count_option@ matching ‘${visibility}’.
+ Existem %#@count_option@ opções correspondentes a ‘${visibility}’.
count_option
NSStringFormatSpecTypeKey
@@ -29,9 +29,9 @@
NSStringFormatValueTypeKey
%ld
one
- 1 option
+ 1 opção
other
- %ld options
+ %ld opções
diff --git a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift
index 76dc722f6..beba6cb3f 100644
--- a/Localization/StringsConvertor/Sources/StringsConvertor/main.swift
+++ b/Localization/StringsConvertor/Sources/StringsConvertor/main.swift
@@ -47,6 +47,7 @@ private func convert(from inputDirectoryURL: URL, to outputDirectory: URL) {
private func map(language: String) -> String? {
switch language {
+ case "Base.lproj": return "Base"
case "ar.lproj": return "ar" // Arabic
case "eu.lproj": return "eu" // Basque
case "ca.lproj": return "ca" // Catalan
diff --git a/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict
new file mode 100644
index 000000000..cd97825f4
--- /dev/null
+++ b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict
@@ -0,0 +1,631 @@
+
+
+
+
+ a11y.plural.count.unread.notification
+
+ NSStringLocalizedFormatKey
+ %#@notification_count_unread_notification@
+ notification_count_unread_notification
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no unread notification
+ one
+ 1 unread notification
+ few
+ %ld unread notifications
+ many
+ %ld unread notification
+ other
+ %ld unread notification
+
+
+ a11y.plural.count.input_limit_exceeds
+
+ NSStringLocalizedFormatKey
+ Input limit exceeds %#@character_count@
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ a11y.plural.count.input_limit_remains
+
+ NSStringLocalizedFormatKey
+ Input limit remains %#@character_count@
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ a11y.plural.count.characters_left
+
+ NSStringLocalizedFormatKey
+ %#@character_count@ left
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ plural.count.followed_by_and_mutual
+
+ NSStringLocalizedFormatKey
+ %#@names@%#@count_mutual@
+ names
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ other
+
+
+ count_mutual
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ Followed by %1$@
+ one
+ Followed by %1$@, and another mutual
+ few
+ Followed by %1$@, and %ld mutuals
+ many
+ Followed by %1$@, and %ld mutuals
+ other
+ Followed by %1$@, and %ld mutuals
+
+
+ plural.count.metric_formatted.post
+
+ NSStringLocalizedFormatKey
+ %@ %#@post_count@
+ post_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ posts
+ one
+ post
+ few
+ posts
+ many
+ posts
+ other
+ posts
+
+
+ plural.count.media
+
+ NSStringLocalizedFormatKey
+ %#@media_count@
+ media_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 media
+ one
+ 1 media
+ few
+ %ld media
+ many
+ %ld media
+ other
+ %ld media
+
+
+ plural.count.post
+
+ NSStringLocalizedFormatKey
+ %#@post_count@
+ post_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 posts
+ one
+ 1 post
+ few
+ %ld posts
+ many
+ %ld posts
+ other
+ %ld posts
+
+
+ plural.count.favorite
+
+ NSStringLocalizedFormatKey
+ %#@favorite_count@
+ favorite_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 favorites
+ one
+ 1 favorite
+ few
+ %ld favorites
+ many
+ %ld favorites
+ other
+ %ld favorites
+
+
+ plural.count.reblog
+
+ NSStringLocalizedFormatKey
+ %#@reblog_count@
+ reblog_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 reblogs
+ one
+ 1 reblog
+ few
+ %ld reblogs
+ many
+ %ld reblogs
+ other
+ %ld reblogs
+
+
+ plural.count.reply
+
+ NSStringLocalizedFormatKey
+ %#@reply_count@
+ reply_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 replies
+ one
+ 1 reply
+ few
+ %ld replies
+ many
+ %ld replies
+ other
+ %ld replies
+
+
+ plural.count.vote
+
+ NSStringLocalizedFormatKey
+ %#@vote_count@
+ vote_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 votes
+ one
+ 1 vote
+ few
+ %ld votes
+ many
+ %ld votes
+ other
+ %ld votes
+
+
+ plural.count.voter
+
+ NSStringLocalizedFormatKey
+ %#@voter_count@
+ voter_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 voters
+ one
+ 1 voter
+ few
+ %ld voters
+ many
+ %ld voters
+ other
+ %ld voters
+
+
+ plural.people_talking
+
+ NSStringLocalizedFormatKey
+ %#@count_people_talking@
+ count_people_talking
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 people talking
+ one
+ 1 people talking
+ few
+ %ld people talking
+ many
+ %ld people talking
+ other
+ %ld people talking
+
+
+ plural.count.following
+
+ NSStringLocalizedFormatKey
+ %#@count_following@
+ count_following
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 following
+ one
+ 1 following
+ few
+ %ld following
+ many
+ %ld following
+ other
+ %ld following
+
+
+ plural.count.follower
+
+ NSStringLocalizedFormatKey
+ %#@count_follower@
+ count_follower
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 followers
+ one
+ 1 follower
+ few
+ %ld followers
+ many
+ %ld followers
+ other
+ %ld followers
+
+
+ date.year.left
+
+ NSStringLocalizedFormatKey
+ %#@count_year_left@
+ count_year_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 years left
+ one
+ 1 year left
+ few
+ %ld years left
+ many
+ %ld years left
+ other
+ %ld years left
+
+
+ date.month.left
+
+ NSStringLocalizedFormatKey
+ %#@count_month_left@
+ count_month_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 months left
+ one
+ 1 months left
+ few
+ %ld months left
+ many
+ %ld months left
+ other
+ %ld months left
+
+
+ date.day.left
+
+ NSStringLocalizedFormatKey
+ %#@count_day_left@
+ count_day_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 days left
+ one
+ 1 day left
+ few
+ %ld days left
+ many
+ %ld days left
+ other
+ %ld days left
+
+
+ date.hour.left
+
+ NSStringLocalizedFormatKey
+ %#@count_hour_left@
+ count_hour_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 hours left
+ one
+ 1 hour left
+ few
+ %ld hours left
+ many
+ %ld hours left
+ other
+ %ld hours left
+
+
+ date.minute.left
+
+ NSStringLocalizedFormatKey
+ %#@count_minute_left@
+ count_minute_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 minutes left
+ one
+ 1 minute left
+ few
+ %ld minutes left
+ many
+ %ld minutes left
+ other
+ %ld minutes left
+
+
+ date.second.left
+
+ NSStringLocalizedFormatKey
+ %#@count_second_left@
+ count_second_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 seconds left
+ one
+ 1 second left
+ few
+ %ld seconds left
+ many
+ %ld seconds left
+ other
+ %ld seconds left
+
+
+ date.year.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_year_ago_abbr@
+ count_year_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0y ago
+ one
+ 1y ago
+ few
+ %ldy ago
+ many
+ %ldy ago
+ other
+ %ldy ago
+
+
+ date.month.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_month_ago_abbr@
+ count_month_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0M ago
+ one
+ 1M ago
+ few
+ %ldM ago
+ many
+ %ldM ago
+ other
+ %ldM ago
+
+
+ date.day.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_day_ago_abbr@
+ count_day_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0d ago
+ one
+ 1d ago
+ few
+ %ldd ago
+ many
+ %ldd ago
+ other
+ %ldd ago
+
+
+ date.hour.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_hour_ago_abbr@
+ count_hour_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0h ago
+ one
+ 1h ago
+ few
+ %ldh ago
+ many
+ %ldh ago
+ other
+ %ldh ago
+
+
+ date.minute.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_minute_ago_abbr@
+ count_minute_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0m ago
+ one
+ 1m ago
+ few
+ %ldm ago
+ many
+ %ldm ago
+ other
+ %ldm ago
+
+
+ date.second.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_second_ago_abbr@
+ count_second_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0s ago
+ one
+ 1s ago
+ few
+ %lds ago
+ many
+ %lds ago
+ other
+ %lds ago
+
+
+
+
diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json
new file mode 100644
index 000000000..f90b2bef6
--- /dev/null
+++ b/Localization/StringsConvertor/input/Base.lproj/app.json
@@ -0,0 +1,722 @@
+{
+ "common": {
+ "alerts": {
+ "common": {
+ "please_try_again": "Please try again.",
+ "please_try_again_later": "Please try again later."
+ },
+ "sign_up_failure": {
+ "title": "Sign Up Failure"
+ },
+ "server_error": {
+ "title": "Server Error"
+ },
+ "vote_failure": {
+ "title": "Vote Failure",
+ "poll_ended": "The poll has ended"
+ },
+ "discard_post_content": {
+ "title": "Discard Draft",
+ "message": "Confirm to discard composed post content."
+ },
+ "publish_post_failure": {
+ "title": "Publish Failure",
+ "message": "Failed to publish the post.\nPlease check your internet connection.",
+ "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."
+ }
+ },
+ "edit_profile_failure": {
+ "title": "Edit Profile Error",
+ "message": "Cannot edit profile. Please try again."
+ },
+ "sign_out": {
+ "title": "Sign Out",
+ "message": "Are you sure you want to sign out?",
+ "confirm": "Sign Out"
+ },
+ "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": "Block Domain"
+ },
+ "save_photo_failure": {
+ "title": "Save Photo Failure",
+ "message": "Please enable the photo library access permission to save the photo."
+ },
+ "delete_post": {
+ "title": "Delete Post",
+ "message": "Are you sure you want to delete this post?"
+ },
+ "clean_cache": {
+ "title": "Clean Cache",
+ "message": "Successfully cleaned %s cache."
+ }
+ },
+ "controls": {
+ "actions": {
+ "back": "Back",
+ "next": "Next",
+ "previous": "Previous",
+ "open": "Open",
+ "add": "Add",
+ "remove": "Remove",
+ "edit": "Edit",
+ "save": "Save",
+ "ok": "OK",
+ "done": "Done",
+ "confirm": "Confirm",
+ "continue": "Continue",
+ "compose": "Compose",
+ "cancel": "Cancel",
+ "discard": "Discard",
+ "try_again": "Try Again",
+ "take_photo": "Take Photo",
+ "save_photo": "Save Photo",
+ "copy_photo": "Copy Photo",
+ "sign_in": "Sign In",
+ "sign_up": "Sign Up",
+ "see_more": "See More",
+ "preview": "Preview",
+ "share": "Share",
+ "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",
+ "reply": "Reply",
+ "report_user": "Report %s",
+ "block_domain": "Block %s",
+ "unblock_domain": "Unblock %s",
+ "settings": "Settings",
+ "delete": "Delete"
+ },
+ "tabs": {
+ "home": "Home",
+ "search": "Search",
+ "notifications": "Notifications",
+ "profile": "Profile"
+ },
+ "keyboard": {
+ "common": {
+ "switch_to_tab": "Switch to %s",
+ "compose_new_post": "Compose New Post",
+ "show_favorites": "Show Favorites",
+ "open_settings": "Open Settings"
+ },
+ "timeline": {
+ "previous_status": "Previous Post",
+ "next_status": "Next Post",
+ "open_status": "Open Post",
+ "open_author_profile": "Open Author's Profile",
+ "open_reblogger_profile": "Open Reblogger's Profile",
+ "reply_status": "Reply to Post",
+ "toggle_reblog": "Toggle Reblog on Post",
+ "toggle_favorite": "Toggle Favorite on Post",
+ "toggle_content_warning": "Toggle Content Warning",
+ "preview_image": "Preview Image"
+ },
+ "segmented_control": {
+ "previous_section": "Previous Section",
+ "next_section": "Next Section"
+ }
+ },
+ "status": {
+ "user_reblogged": "%s reblogged",
+ "user_replied_to": "Replied to %s",
+ "show_post": "Show Post",
+ "show_user_profile": "Show user profile",
+ "content_warning": "Content Warning",
+ "sensitive_content": "Sensitive Content",
+ "media_content_warning": "Tap anywhere to reveal",
+ "tap_to_reveal": "Tap to reveal",
+ "poll": {
+ "vote": "Vote",
+ "closed": "Closed"
+ },
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
+ "actions": {
+ "reply": "Reply",
+ "reblog": "Reblog",
+ "unreblog": "Undo reblog",
+ "favorite": "Favorite",
+ "unfavorite": "Unfavorite",
+ "menu": "Menu",
+ "hide": "Hide",
+ "show_image": "Show image",
+ "show_gif": "Show GIF",
+ "show_video_player": "Show video player",
+ "tap_then_hold_to_show_menu": "Tap then hold to show menu"
+ },
+ "tag": {
+ "url": "URL",
+ "mention": "Mention",
+ "link": "Link",
+ "hashtag": "Hashtag",
+ "email": "Email",
+ "emoji": "Emoji"
+ },
+ "visibility": {
+ "unlisted": "Everyone can see this post but not display in the public timeline.",
+ "private": "Only their followers can see this post.",
+ "private_from_me": "Only my followers can see this post.",
+ "direct": "Only mentioned user can see this post."
+ }
+ },
+ "friendship": {
+ "follow": "Follow",
+ "following": "Following",
+ "request": "Request",
+ "pending": "Pending",
+ "block": "Block",
+ "block_user": "Block %s",
+ "block_domain": "Block %s",
+ "unblock": "Unblock",
+ "unblock_user": "Unblock %s",
+ "blocked": "Blocked",
+ "mute": "Mute",
+ "mute_user": "Mute %s",
+ "unmute": "Unmute",
+ "unmute_user": "Unmute %s",
+ "muted": "Muted",
+ "edit_info": "Edit Info",
+ "show_reblogs": "Show Reblogs",
+ "hide_reblogs": "Hide Reblogs"
+ },
+ "timeline": {
+ "filtered": "Filtered",
+ "timestamp": {
+ "now": "Now"
+ },
+ "loader": {
+ "load_missing_posts": "Load missing posts",
+ "loading_missing_posts": "Loading missing posts...",
+ "show_more_replies": "Show more replies"
+ },
+ "header": {
+ "no_status_found": "No Post 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."
+ }
+ }
+ }
+ },
+ "scene": {
+ "welcome": {
+ "slogan": "Social networking\nback in your hands.",
+ "get_started": "Get Started",
+ "log_in": "Log In"
+ },
+ "server_picker": {
+ "title": "Mastodon is made of users in different servers.",
+ "subtitle": "Pick a server based on your interests, region, or a general purpose one.",
+ "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.",
+ "button": {
+ "category": {
+ "all": "All",
+ "all_accessiblity_description": "Category: All",
+ "academia": "academia",
+ "activism": "activism",
+ "food": "food",
+ "furry": "furry",
+ "games": "games",
+ "general": "general",
+ "journalism": "journalism",
+ "lgbt": "lgbt",
+ "regional": "regional",
+ "art": "art",
+ "music": "music",
+ "tech": "tech"
+ },
+ "see_less": "See Less",
+ "see_more": "See More"
+ },
+ "label": {
+ "language": "LANGUAGE",
+ "users": "USERS",
+ "category": "CATEGORY"
+ },
+ "input": {
+ "placeholder": "Search servers",
+ "search_servers_or_enter_url": "Search servers or enter URL"
+ },
+ "empty_state": {
+ "finding_servers": "Finding available servers...",
+ "bad_network": "Something went wrong while loading the data. Check your internet connection.",
+ "no_results": "No results"
+ }
+ },
+ "register": {
+ "title": "Let’s get you set up on %s",
+ "lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
+ "input": {
+ "avatar": {
+ "delete": "Delete"
+ },
+ "username": {
+ "placeholder": "username",
+ "duplicate_prompt": "This username is taken."
+ },
+ "display_name": {
+ "placeholder": "display name"
+ },
+ "email": {
+ "placeholder": "email"
+ },
+ "password": {
+ "placeholder": "password",
+ "require": "Your password needs at least:",
+ "character_limit": "8 characters",
+ "accessibility": {
+ "checked": "checked",
+ "unchecked": "unchecked"
+ },
+ "hint": "Your password needs at least eight characters"
+ },
+ "invite": {
+ "registration_user_invite_request": "Why do you want to join?"
+ }
+ },
+ "error": {
+ "item": {
+ "username": "Username",
+ "email": "Email",
+ "password": "Password",
+ "agreement": "Agreement",
+ "locale": "Locale",
+ "reason": "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",
+ "blank": "%s is required",
+ "invalid": "%s is invalid",
+ "too_long": "%s is too long",
+ "too_short": "%s is too short",
+ "inclusion": "%s is not a supported value"
+ },
+ "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)",
+ "email_invalid": "This is not a valid email address",
+ "password_too_short": "Password is too short (must be at least 8 characters)"
+ }
+ }
+ },
+ "server_rules": {
+ "title": "Some ground rules.",
+ "subtitle": "These are set and enforced by the %s moderators.",
+ "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.",
+ "terms_of_service": "terms of service",
+ "privacy_policy": "privacy policy",
+ "button": {
+ "confirm": "I Agree"
+ }
+ },
+ "confirm_email": {
+ "title": "One last thing.",
+ "subtitle": "Tap the link we emailed to you to verify your account.",
+ "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account",
+ "button": {
+ "open_email_app": "Open Email App",
+ "resend": "Resend"
+ },
+ "dont_receive_email": {
+ "title": "Check your email",
+ "description": "Check if your email address is correct as well as your junk folder if you haven’t.",
+ "resend_email": "Resend Email"
+ },
+ "open_email_app": {
+ "title": "Check your inbox.",
+ "description": "We just sent you an email. Check your junk folder if you haven’t.",
+ "mail": "Mail",
+ "open_email_client": "Open Email Client"
+ }
+ },
+ "home_timeline": {
+ "title": "Home",
+ "navigation_bar_state": {
+ "offline": "Offline",
+ "new_posts": "See new posts",
+ "published": "Published!",
+ "Publishing": "Publishing post...",
+ "accessibility": {
+ "logo_label": "Logo Button",
+ "logo_hint": "Tap to scroll to top and tap again to previous location"
+ }
+ }
+ },
+ "suggestion_account": {
+ "title": "Find People to Follow",
+ "follow_explain": "When you follow someone, you’ll see their posts in your home feed."
+ },
+ "compose": {
+ "title": {
+ "new_post": "New Post",
+ "new_reply": "New Reply"
+ },
+ "media_selection": {
+ "camera": "Take Photo",
+ "photo_library": "Photo Library",
+ "browse": "Browse"
+ },
+ "content_input_placeholder": "Type or paste what’s on your mind",
+ "compose_action": "Publish",
+ "replying_to_user": "replying to %s",
+ "attachment": {
+ "photo": "photo",
+ "video": "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...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not recognize this media attachment",
+ "attachment_too_large": "Attachment too large",
+ "compressing_state": "Compressing...",
+ "server_processing_state": "Server Processing..."
+ },
+ "poll": {
+ "duration_time": "Duration: %s",
+ "thirty_minutes": "30 minutes",
+ "one_hour": "1 Hour",
+ "six_hours": "6 Hours",
+ "one_day": "1 Day",
+ "three_days": "3 Days",
+ "seven_days": "7 Days",
+ "option_number": "Option %ld",
+ "the_poll_is_invalid": "The poll is invalid",
+ "the_poll_has_empty_option": "The poll has empty option"
+ },
+ "content_warning": {
+ "placeholder": "Write an accurate warning here..."
+ },
+ "visibility": {
+ "public": "Public",
+ "unlisted": "Unlisted",
+ "private": "Followers only",
+ "direct": "Only people I mention"
+ },
+ "auto_complete": {
+ "space_to_add": "Space to add"
+ },
+ "accessibility": {
+ "append_attachment": "Add Attachment",
+ "append_poll": "Add Poll",
+ "remove_poll": "Remove Poll",
+ "custom_emoji_picker": "Custom Emoji Picker",
+ "enable_content_warning": "Enable Content Warning",
+ "disable_content_warning": "Disable Content Warning",
+ "post_visibility_menu": "Post Visibility Menu",
+ "post_options": "Post Options",
+ "posting_as": "Posting as %s"
+ },
+ "keyboard": {
+ "discard_post": "Discard Post",
+ "publish_post": "Publish Post",
+ "toggle_poll": "Toggle Poll",
+ "toggle_content_warning": "Toggle Content Warning",
+ "append_attachment_entry": "Add Attachment - %s",
+ "select_visibility_entry": "Select Visibility - %s"
+ }
+ },
+ "profile": {
+ "header": {
+ "follows_you": "Follows You"
+ },
+ "dashboard": {
+ "posts": "posts",
+ "following": "following",
+ "followers": "followers"
+ },
+ "fields": {
+ "add_row": "Add Row",
+ "placeholder": {
+ "label": "Label",
+ "content": "Content"
+ },
+ "verified": {
+ "short": "Verified on %s",
+ "long": "Ownership of this link was checked on %s"
+ }
+ },
+ "segmented_control": {
+ "posts": "Posts",
+ "replies": "Replies",
+ "posts_and_replies": "Posts and Replies",
+ "media": "Media",
+ "about": "About"
+ },
+ "relationship_action_alert": {
+ "confirm_mute_user": {
+ "title": "Mute Account",
+ "message": "Confirm to mute %s"
+ },
+ "confirm_unmute_user": {
+ "title": "Unmute Account",
+ "message": "Confirm to unmute %s"
+ },
+ "confirm_block_user": {
+ "title": "Block Account",
+ "message": "Confirm to block %s"
+ },
+ "confirm_unblock_user": {
+ "title": "Unblock Account",
+ "message": "Confirm to unblock %s"
+ },
+ "confirm_show_reblogs": {
+ "title": "Show Reblogs",
+ "message": "Confirm to show reblogs"
+ },
+ "confirm_hide_reblogs": {
+ "title": "Hide Reblogs",
+ "message": "Confirm to hide reblogs"
+ }
+ },
+ "accessibility": {
+ "show_avatar_image": "Show avatar image",
+ "edit_avatar_image": "Edit avatar image",
+ "show_banner_image": "Show banner image",
+ "double_tap_to_open_the_list": "Double tap to open the list"
+ }
+ },
+ "follower": {
+ "title": "follower",
+ "footer": "Followers from other servers are not displayed."
+ },
+ "following": {
+ "title": "following",
+ "footer": "Follows from other servers are not displayed."
+ },
+ "familiarFollowers": {
+ "title": "Followers you familiar",
+ "followed_by_names": "Followed by %s"
+ },
+ "favorited_by": {
+ "title": "Favorited By"
+ },
+ "reblogged_by": {
+ "title": "Reblogged By"
+ },
+ "search": {
+ "title": "Search",
+ "search_bar": {
+ "placeholder": "Search hashtags and users",
+ "cancel": "Cancel"
+ },
+ "recommend": {
+ "button_text": "See All",
+ "hash_tag": {
+ "title": "Trending on Mastodon",
+ "description": "Hashtags that are getting quite a bit of attention",
+ "people_talking": "%s people are talking"
+ },
+ "accounts": {
+ "title": "Accounts you might like",
+ "description": "You may like to follow these accounts",
+ "follow": "Follow"
+ }
+ },
+ "searching": {
+ "segment": {
+ "all": "All",
+ "people": "People",
+ "hashtags": "Hashtags",
+ "posts": "Posts"
+ },
+ "empty_state": {
+ "no_results": "No results"
+ },
+ "recent_search": "Recent searches",
+ "clear": "Clear"
+ }
+ },
+ "discovery": {
+ "tabs": {
+ "posts": "Posts",
+ "hashtags": "Hashtags",
+ "news": "News",
+ "community": "Community",
+ "for_you": "For You"
+ },
+ "intro": "These are the posts gaining traction in your corner of Mastodon."
+ },
+ "favorite": {
+ "title": "Your Favorites"
+ },
+ "notification": {
+ "title": {
+ "Everything": "Everything",
+ "Mentions": "Mentions"
+ },
+ "notification_description": {
+ "followed_you": "followed you",
+ "favorited_your_post": "favorited your post",
+ "reblogged_your_post": "reblogged your post",
+ "mentioned_you": "mentioned you",
+ "request_to_follow_you": "request to follow you",
+ "poll_has_ended": "poll has ended"
+ },
+ "keyobard": {
+ "show_everything": "Show Everything",
+ "show_mentions": "Show Mentions"
+ },
+ "follow_request": {
+ "accept": "Accept",
+ "accepted": "Accepted",
+ "reject": "reject",
+ "rejected": "Rejected"
+ }
+ },
+ "thread": {
+ "back_title": "Post",
+ "title": "Post from %s"
+ },
+ "settings": {
+ "title": "Settings",
+ "section": {
+ "appearance": {
+ "title": "Appearance",
+ "automatic": "Automatic",
+ "light": "Always Light",
+ "dark": "Always Dark"
+ },
+ "look_and_feel": {
+ "title": "Look and Feel",
+ "use_system": "Use System",
+ "really_dark": "Really Dark",
+ "sorta_dark": "Sorta Dark",
+ "light": "Light"
+ },
+ "notifications": {
+ "title": "Notifications",
+ "favorites": "Favorites my post",
+ "follows": "Follows me",
+ "boosts": "Reblogs my post",
+ "mentions": "Mentions me",
+ "trigger": {
+ "anyone": "anyone",
+ "follower": "a follower",
+ "follow": "anyone I follow",
+ "noone": "no one",
+ "title": "Notify me when"
+ }
+ },
+ "preference": {
+ "title": "Preferences",
+ "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",
+ "open_links_in_mastodon": "Open links in Mastodon"
+ },
+ "boring_zone": {
+ "title": "The Boring Zone",
+ "account_settings": "Account Settings",
+ "terms": "Terms of Service",
+ "privacy": "Privacy Policy"
+ },
+ "spicy_zone": {
+ "title": "The Spicy Zone",
+ "clear": "Clear Media Cache",
+ "signout": "Sign Out"
+ }
+ },
+ "footer": {
+ "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
+ },
+ "keyboard": {
+ "close_settings_window": "Close Settings Window"
+ }
+ },
+ "report": {
+ "title_report": "Report",
+ "title": "Report %s",
+ "step1": "Step 1 of 2",
+ "step2": "Step 2 of 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?",
+ "report_sent_title": "Thanks for reporting, we’ll look into this.",
+ "send": "Send Report",
+ "skip_to_send": "Send without comment",
+ "text_placeholder": "Type or paste additional comments",
+ "reported": "REPORTED",
+ "step_one": {
+ "step_1_of_4": "Step 1 of 4",
+ "whats_wrong_with_this_post": "What's wrong with this post?",
+ "whats_wrong_with_this_account": "What's wrong with this account?",
+ "whats_wrong_with_this_username": "What's wrong with %s?",
+ "select_the_best_match": "Select the best match",
+ "i_dont_like_it": "I don’t like it",
+ "it_is_not_something_you_want_to_see": "It is not something you want to see",
+ "its_spam": "It’s spam",
+ "malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies",
+ "it_violates_server_rules": "It violates server rules",
+ "you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules",
+ "its_something_else": "It’s something else",
+ "the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories"
+ },
+ "step_two": {
+ "step_2_of_4": "Step 2 of 4",
+ "which_rules_are_being_violated": "Which rules are being violated?",
+ "select_all_that_apply": "Select all that apply",
+ "i_just_don’t_like_it": "I just don’t like it"
+ },
+ "step_three": {
+ "step_3_of_4": "Step 3 of 4",
+ "are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?",
+ "select_all_that_apply": "Select all that apply"
+ },
+ "step_four": {
+ "step_4_of_4": "Step 4 of 4",
+ "is_there_anything_else_we_should_know": "Is there anything else we should know?"
+ },
+ "step_final": {
+ "dont_want_to_see_this": "Don’t want to see this?",
+ "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.",
+ "unfollow": "Unfollow",
+ "unfollowed": "Unfollowed",
+ "unfollow_user": "Unfollow %s",
+ "mute_user": "Mute %s",
+ "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.",
+ "block_user": "Block %s",
+ "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.",
+ "while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s"
+ }
+ },
+ "preview": {
+ "keyboard": {
+ "close_preview": "Close Preview",
+ "show_next": "Show Next",
+ "show_previous": "Show Previous"
+ }
+ },
+ "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"
+ },
+ "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"
+ },
+ "bookmark": {
+ "title": "Bookmarks"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json
new file mode 100644
index 000000000..c6db73de0
--- /dev/null
+++ b/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json
@@ -0,0 +1,6 @@
+{
+ "NSCameraUsageDescription": "Used to take photo for post status",
+ "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library",
+ "NewPostShortcutItemTitle": "New Post",
+ "SearchShortcutItemTitle": "Search"
+}
diff --git a/Localization/StringsConvertor/input/ar.lproj/app.json b/Localization/StringsConvertor/input/ar.lproj/app.json
index ce68229ac..4c5ac4c8b 100644
--- a/Localization/StringsConvertor/input/ar.lproj/app.json
+++ b/Localization/StringsConvertor/input/ar.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "صَوِّت",
"closed": "انتهى"
},
+ "meta_entity": {
+ "url": "رابِط: %s",
+ "hashtag": "وَسْم: %s",
+ "mention": "إظهار المِلف التعريفي: %s",
+ "email": "عُنوان البريد الإلكتُروني: %s"
+ },
"actions": {
"reply": "الرَّد",
"reblog": "إعادة النشر",
@@ -181,8 +187,8 @@
"unmute_user": "رفع الكتم عن %s",
"muted": "مكتوم",
"edit_info": "تَحريرُ المَعلُومات",
- "show_reblogs": "Show Reblogs",
- "hide_reblogs": "Hide Reblogs"
+ "show_reblogs": "إظهار إعادات التدوين",
+ "hide_reblogs": "إخفاء إعادات التدوين"
},
"timeline": {
"filtered": "مُصفَّى",
@@ -376,7 +382,11 @@
"video": "مقطع مرئي",
"attachment_broken": "هذا ال%s مُعطَّل\nويتعذَّرُ رفعُه إلى ماستودون.",
"description_photo": "صِف الصورة للمَكفوفين...",
- "description_video": "صِف المقطع المرئي للمَكفوفين..."
+ "description_video": "صِف المقطع المرئي للمَكفوفين...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "المُدَّة: %s",
@@ -459,12 +469,12 @@
"message": "تأكيدُ رَفع الحَظرِ عَن %s"
},
"confirm_show_reblogs": {
- "title": "Show Reblogs",
- "message": "Confirm to show reblogs"
+ "title": "إظهار إعادات التدوين",
+ "message": "التأكيد لِإظهار إعادات التدوين"
},
"confirm_hide_reblogs": {
- "title": "Hide Reblogs",
- "message": "Confirm to hide reblogs"
+ "title": "إخفاء إعادات التدوين",
+ "message": "التأكيد لِإخفاء إعادات التدوين"
}
},
"accessibility": {
@@ -696,7 +706,7 @@
"accessibility_hint": "انقر نقرًا مزدوجًا لتجاهُل النافذة المنبثقة"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "العَلاماتُ المَرجعيَّة"
}
}
}
diff --git a/Localization/StringsConvertor/input/ca.lproj/app.json b/Localization/StringsConvertor/input/ca.lproj/app.json
index 4766eb31b..7164d1d12 100644
--- a/Localization/StringsConvertor/input/ca.lproj/app.json
+++ b/Localization/StringsConvertor/input/ca.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vota",
"closed": "Finalitzada"
},
+ "meta_entity": {
+ "url": "Enllaç: %s",
+ "hashtag": "Etiqueta %s",
+ "mention": "Mostra el Perfil: %s",
+ "email": "Correu electrònic: %s"
+ },
"actions": {
"reply": "Respon",
"reblog": "Impuls",
@@ -376,7 +382,11 @@
"video": "vídeo",
"attachment_broken": "Aquest %s està trencat i no pot ser\ncarregat a Mastodon.",
"description_photo": "Descriu la foto per als disminuïts visuals...",
- "description_video": "Descriu el vídeo per als disminuïts visuals..."
+ "description_video": "Descriu el vídeo per als disminuïts visuals...",
+ "load_failed": "Ha fallat la càrrega",
+ "upload_failed": "Pujada fallida",
+ "can_not_recognize_this_media_attachment": "No es pot reconèixer l'adjunt multimèdia",
+ "attachment_too_large": "El fitxer adjunt és massa gran"
},
"poll": {
"duration_time": "Durada: %s",
@@ -696,7 +706,7 @@
"accessibility_hint": "Toca dues vegades per descartar l'assistent"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Marcadors"
}
}
}
diff --git a/Localization/StringsConvertor/input/ckb.lproj/app.json b/Localization/StringsConvertor/input/ckb.lproj/app.json
index 49f72d7a3..25452f38b 100644
--- a/Localization/StringsConvertor/input/ckb.lproj/app.json
+++ b/Localization/StringsConvertor/input/ckb.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "دەنگ بدە",
"closed": "داخراوە"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "وەڵامی بدەوە",
"reblog": "پۆستی بکەوە",
@@ -376,7 +382,11 @@
"video": "ڤیدیۆ",
"attachment_broken": "ئەم %sـە تێک چووە و ناتوانیت بەرزی بکەیتەوە.",
"description_photo": "وێنەکەت بۆ نابیناکان باس بکە...",
- "description_video": "ڤیدیۆکەت بۆ نابیناکان باس بکە..."
+ "description_video": "ڤیدیۆکەت بۆ نابیناکان باس بکە...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "کات: %s",
diff --git a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict
index cdf35477e..827bd79e6 100644
--- a/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/cs.lproj/Localizable.stringsdict
@@ -13,19 +13,19 @@
NSStringFormatValueTypeKey
ld
one
- 1 unread notification
+ 1 nepřečtené oznámení
few
- %ld unread notification
+ %ld nepřečtené oznámení
many
- %ld unread notification
+ %ld nepřečtených oznámení
other
- %ld unread notification
+ %ld nepřečtených oznámení
a11y.plural.count.input_limit_exceeds
NSStringLocalizedFormatKey
- Input limit exceeds %#@character_count@
+ Vstupní limit přesahuje %#@character_count@
character_count
NSStringFormatSpecTypeKey
@@ -33,19 +33,19 @@
NSStringFormatValueTypeKey
ld
one
- 1 character
+ 1 znak
few
- %ld characters
+ %ld znaky
many
- %ld characters
+ %ld znaků
other
- %ld characters
+ %ld znaků
a11y.plural.count.input_limit_remains
NSStringLocalizedFormatKey
- Input limit remains %#@character_count@
+ Vstupní limit zůstává %#@character_count@
character_count
NSStringFormatSpecTypeKey
@@ -53,13 +53,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 character
+ 1 znak
few
- %ld characters
+ %ld znaky
many
- %ld characters
+ %ld znaků
other
- %ld characters
+ %ld znaků
plural.count.followed_by_and_mutual
@@ -108,13 +108,13 @@
NSStringFormatValueTypeKey
ld
one
- post
+ příspěvek
few
- posts
+ příspěvky
many
- posts
+ příspěvků
other
- posts
+ příspěvků
plural.count.media
@@ -128,13 +128,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 media
+ 1 médium
few
- %ld media
+ %ld média
many
- %ld media
+ %ld médií
other
- %ld media
+ %ld médií
plural.count.post
@@ -148,13 +148,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 post
+ 1 příspěvek
few
- %ld posts
+ %ld příspěvky
many
- %ld posts
+ %ld příspěvků
other
- %ld posts
+ %ld příspěvků
plural.count.favorite
@@ -168,7 +168,7 @@
NSStringFormatValueTypeKey
ld
one
- 1 favorite
+ 1 oblíbený
few
%ld favorites
many
@@ -208,13 +208,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 reply
+ 1 odpověď
few
- %ld replies
+ %ld odpovědi
many
- %ld replies
+ %ld odpovědí
other
- %ld replies
+ %ld odpovědí
plural.count.vote
@@ -228,13 +228,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 vote
+ 1 hlas
few
- %ld votes
+ %ld hlasy
many
- %ld votes
+ %ld hlasů
other
- %ld votes
+ %ld hlasů
plural.count.voter
@@ -248,13 +248,13 @@
NSStringFormatValueTypeKey
ld
one
- 1 voter
+ 1 hlasující
few
- %ld voters
+ %ld hlasující
many
- %ld voters
+ %ld hlasujících
other
- %ld voters
+ %ld hlasujících
plural.people_talking
diff --git a/Localization/StringsConvertor/input/cs.lproj/app.json b/Localization/StringsConvertor/input/cs.lproj/app.json
index 550f71808..4050352f4 100644
--- a/Localization/StringsConvertor/input/cs.lproj/app.json
+++ b/Localization/StringsConvertor/input/cs.lproj/app.json
@@ -37,12 +37,12 @@
"confirm": "Odhlásit se"
},
"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.",
+ "title": "Opravdu chcete blokovat celou doménu %s? Ve většině případů stačí zablokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.",
"block_entire_domain": "Blokovat doménu"
},
"save_photo_failure": {
"title": "Uložení fotografie se nezdařilo",
- "message": "Please enable the photo library access permission to save the photo."
+ "message": "Pro uložení fotografie povolte přístup k knihovně fotografií."
},
"delete_post": {
"title": "Odstranit příspěvek",
@@ -79,7 +79,7 @@
"see_more": "Zobrazit více",
"preview": "Náhled",
"share": "Sdílet",
- "share_user": "Share %s",
+ "share_user": "Sdílet %s",
"share_post": "Sdílet příspěvek",
"open_in_safari": "Otevřít v Safari",
"open_in_browser": "Otevřít v prohlížeči",
@@ -125,7 +125,7 @@
},
"status": {
"user_reblogged": "%s reblogged",
- "user_replied_to": "Replied to %s",
+ "user_replied_to": "Odpověděl %s",
"show_post": "Zobrazit příspěvek",
"show_user_profile": "Zobrazit profil uživatele",
"content_warning": "Varování o obsahu",
@@ -136,18 +136,24 @@
"vote": "Hlasovat",
"closed": "Uzavřeno"
},
+ "meta_entity": {
+ "url": "Odkaz: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Zobrazit profil: %s",
+ "email": "E-mailová adresa: %s"
+ },
"actions": {
"reply": "Odpovědět",
"reblog": "Boostnout",
"unreblog": "Undo reblog",
- "favorite": "Favorite",
+ "favorite": "Oblíbit",
"unfavorite": "Odebrat z oblízených",
"menu": "Nabídka",
"hide": "Skrýt",
"show_image": "Zobrazit obrázek",
"show_gif": "Zobrazit GIF",
"show_video_player": "Zobrazit video přehrávač",
- "tap_then_hold_to_show_menu": "Tap then hold to show menu"
+ "tap_then_hold_to_show_menu": "Klepnutím podržte pro zobrazení nabídky"
},
"tag": {
"url": "URL",
@@ -159,22 +165,22 @@
},
"visibility": {
"unlisted": "Každý může vidět tento příspěvek, ale nezobrazovat ve veřejné časové ose.",
- "private": "Only their followers can see this post.",
- "private_from_me": "Only my followers can see this post.",
- "direct": "Only mentioned user can see this post."
+ "private": "Pouze jejich sledující mohou vidět tento příspěvek.",
+ "private_from_me": "Pouze moji sledující mohou vidět tento příspěvek.",
+ "direct": "Pouze zmíněný uživatel může vidět tento příspěvek."
}
},
"friendship": {
"follow": "Sledovat",
- "following": "Following",
- "request": "Request",
+ "following": "Sleduji",
+ "request": "Požadavek",
"pending": "Čekající",
"block": "Blokovat",
"block_user": "Blokovat %s",
"block_domain": "Blokovat %s",
"unblock": "Odblokovat",
"unblock_user": "Odblokovat %s",
- "blocked": "Blocked",
+ "blocked": "Blokovaný",
"mute": "Skrýt",
"mute_user": "Skrýt %s",
"unmute": "Odkrýt",
@@ -185,7 +191,7 @@
"hide_reblogs": "Hide Reblogs"
},
"timeline": {
- "filtered": "Filtered",
+ "filtered": "Filtrováno",
"timestamp": {
"now": "Nyní"
},
@@ -196,26 +202,26 @@
},
"header": {
"no_status_found": "Nebyl nalezen žádný příspěvek",
- "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": "Nemůžete zobrazit profil tohoto uživatele, dokud ho neodblokujete.\nVáš profil pro něj vypadá takto.",
+ "user_blocking_warning": "Nemůžete zobrazit profil %s, dokud ho neodblokujete.\nVáš profil pro něj vypadá takto.",
+ "blocked_warning": "Nemůžeš zobrazit profil tohoto uživatele, dokud tě neodblokují.",
+ "user_blocked_warning": "Nemůžete zobrazit profil %s, dokud vás neodblokuje.",
+ "suspended_warning": "Tento uživatel byl pozastaven.",
+ "user_suspended_warning": "Účet %s byl pozastaven."
}
}
}
},
"scene": {
"welcome": {
- "slogan": "Social networking\nback in your hands.",
+ "slogan": "Sociální sítě opět ve vašich rukou.",
"get_started": "Začínáme",
"log_in": "Přihlásit se"
},
"server_picker": {
"title": "Mastodon tvoří uživatelé z různých serverů.",
- "subtitle": "Pick a server based on your interests, region, or a general purpose one.",
- "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.",
+ "subtitle": "Vyberte server založený na vašich zájmech, regionu nebo obecném účelu.",
+ "subtitle_extend": "Vyberte server založený na vašich zájmech, regionu nebo obecném účelu. Každý server je provozován zcela nezávislou organizací nebo jednotlivcem.",
"button": {
"category": {
"all": "Vše",
@@ -224,7 +230,7 @@
"activism": "aktivismus",
"food": "jídlo",
"furry": "furry",
- "games": "games",
+ "games": "hry",
"general": "obecné",
"journalism": "žurnalistika",
"lgbt": "lgbt",
@@ -252,8 +258,8 @@
}
},
"register": {
- "title": "Let’s get you set up on %s",
- "lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
+ "title": "Pojďme si nastavit %s",
+ "lets_get_you_set_up_on_domain": "Pojďme si nastavit %s",
"input": {
"avatar": {
"delete": "Smazat"
@@ -292,51 +298,51 @@
"reason": "Důvod"
},
"reason": {
- "blocked": "%s contains a disallowed email provider",
- "unreachable": "%s does not seem to exist",
+ "blocked": "%s používá zakázanou e-mailovou službu",
+ "unreachable": "%s pravděpodobně neexistuje",
"taken": "%s se již používá",
"reserved": "%s je rezervované klíčové slovo",
"accepted": "%s musí být přijato",
"blank": "%s je vyžadováno",
- "invalid": "%s is invalid",
- "too_long": "%s is too long",
- "too_short": "%s is too short",
- "inclusion": "%s is not a supported value"
+ "invalid": "%s je neplatné",
+ "too_long": "%s je příliš dlouhé",
+ "too_short": "%s je příliš krátké",
+ "inclusion": "%s není podporovaná hodnota"
},
"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": "Uživatelské jméno musí obsahovat pouze alfanumerické znaky a podtržítka",
+ "username_too_long": "Uživatelské jméno je příliš dlouhé (nemůže být delší než 30 znaků)",
"email_invalid": "Toto není platná e-mailová adresa",
- "password_too_short": "Password is too short (must be at least 8 characters)"
+ "password_too_short": "Heslo je příliš krátké (musí mít alespoň 8 znaků)"
}
}
},
"server_rules": {
- "title": "Some ground rules.",
- "subtitle": "These are set and enforced by the %s moderators.",
- "prompt": "By continuing, you’re subject to the terms of service and privacy policy for %s.",
- "terms_of_service": "terms of service",
- "privacy_policy": "privacy policy",
+ "title": "Některá základní pravidla.",
+ "subtitle": "Ty nastavují a prosazují moderátoři %s.",
+ "prompt": "Pokračováním budete podléhat podmínkám služby a zásad ochrany osobních údajů pro uživatele %s.",
+ "terms_of_service": "podmínky služby",
+ "privacy_policy": "zásady ochrany osobních údajů",
"button": {
- "confirm": "I Agree"
+ "confirm": "Souhlasím"
}
},
"confirm_email": {
- "title": "One last thing.",
- "subtitle": "Tap the link we emailed to you to verify your account.",
- "tap_the_link_we_emailed_to_you_to_verify_your_account": "Tap the link we emailed to you to verify your account",
+ "title": "Ještě jedna věc.",
+ "subtitle": "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet.",
+ "tap_the_link_we_emailed_to_you_to_verify_your_account": "Klepněte na odkaz, který jsme vám poslali e-mailem, abyste ověřili Váš účet",
"button": {
- "open_email_app": "Open Email App",
- "resend": "Resend"
+ "open_email_app": "Otevřít e-mailovou aplikaci",
+ "resend": "Poslat znovu"
},
"dont_receive_email": {
- "title": "Check your email",
- "description": "Check if your email address is correct as well as your junk folder if you haven’t.",
- "resend_email": "Resend Email"
+ "title": "Zkontrolujte svůj e-mail",
+ "description": "Zkontrolujte, zda je vaše e-mailová adresa správná, stejně jako složka nevyžádané pošty, pokud ji máte.",
+ "resend_email": "Znovu odeslat e-mail"
},
"open_email_app": {
- "title": "Check your inbox.",
- "description": "We just sent you an email. Check your junk folder if you haven’t.",
+ "title": "Zkontrolujte doručenou poštu.",
+ "description": "Právě jsme vám poslali e-mail. Zkontrolujte složku nevyžádané zprávy, pokud ji máte.",
"mail": "Pošta",
"open_email_client": "Otevřít e-mailového klienta"
}
@@ -349,37 +355,41 @@
"published": "Publikováno!",
"Publishing": "Publikování příspěvku...",
"accessibility": {
- "logo_label": "Logo Button",
- "logo_hint": "Tap to scroll to top and tap again to previous location"
+ "logo_label": "Tlačítko s logem",
+ "logo_hint": "Klepnutím přejdete nahoru a znovu klepněte na předchozí místo"
}
}
},
"suggestion_account": {
- "title": "Find People to Follow",
- "follow_explain": "When you follow someone, you’ll see their posts in your home feed."
+ "title": "Najít lidi pro sledování",
+ "follow_explain": "Když někoho sledujete, uvidíte jejich příspěvky ve vašem domovském kanálu."
},
"compose": {
"title": {
"new_post": "Nový příspěvek",
- "new_reply": "New Reply"
+ "new_reply": "Nová odpověď"
},
"media_selection": {
- "camera": "Take Photo",
- "photo_library": "Photo Library",
- "browse": "Browse"
+ "camera": "Vyfotit",
+ "photo_library": "Knihovna fotografií",
+ "browse": "Procházet"
},
- "content_input_placeholder": "Type or paste what’s on your mind",
- "compose_action": "Publish",
- "replying_to_user": "replying to %s",
+ "content_input_placeholder": "Napište nebo vložte, co je na mysli",
+ "compose_action": "Zveřejnit",
+ "replying_to_user": "odpovídá na %s",
"attachment": {
- "photo": "photo",
+ "photo": "fotka",
"video": "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": "Tento %s je poškozený a nemůže být\nnahrán do Mastodonu.",
+ "description_photo": "Popište fotografii pro zrakově postižené osoby...",
+ "description_video": "Popište video pro zrakově postižené...",
+ "load_failed": "Načtení se nezdařilo",
+ "upload_failed": "Nahrání selhalo",
+ "can_not_recognize_this_media_attachment": "Nelze rozpoznat toto medium přílohy",
+ "attachment_too_large": "Příloha je příliš velká"
},
"poll": {
- "duration_time": "Duration: %s",
+ "duration_time": "Doba trvání: %s",
"thirty_minutes": "30 minut",
"one_hour": "1 hodina",
"six_hours": "6 hodin",
@@ -389,7 +399,7 @@
"option_number": "Možnost %ld"
},
"content_warning": {
- "placeholder": "Write an accurate warning here..."
+ "placeholder": "Zde napište přesné varování..."
},
"visibility": {
"public": "Veřejný",
@@ -398,7 +408,7 @@
"direct": "Pouze lidé, které zmíním"
},
"auto_complete": {
- "space_to_add": "Space to add"
+ "space_to_add": "Mezera k přidání"
},
"accessibility": {
"append_attachment": "Přidat přílohu",
@@ -413,7 +423,7 @@
"discard_post": "Zahodit příspěvek",
"publish_post": "Publikovat příspěvek",
"toggle_poll": "Přepnout anketu",
- "toggle_content_warning": "Toggle Content Warning",
+ "toggle_content_warning": "Přepnout varování obsahu",
"append_attachment_entry": "Přidat přílohu - %s",
"select_visibility_entry": "Vyberte viditelnost - %s"
}
@@ -424,13 +434,13 @@
},
"dashboard": {
"posts": "příspěvky",
- "following": "following",
+ "following": "sledování",
"followers": "sledující"
},
"fields": {
"add_row": "Přidat řádek",
"placeholder": {
- "label": "Label",
+ "label": "Označení",
"content": "Obsah"
}
},
@@ -439,7 +449,7 @@
"replies": "Odpovědí",
"posts_and_replies": "Příspěvky a odpovědi",
"media": "Média",
- "about": "About"
+ "about": "O uživateli"
},
"relationship_action_alert": {
"confirm_mute_user": {
@@ -448,7 +458,7 @@
},
"confirm_unmute_user": {
"title": "Zrušit skrytí účtu",
- "message": "Confirm to unmute %s"
+ "message": "Potvrďte zrušení ztlumení %s"
},
"confirm_block_user": {
"title": "Blokovat účet",
@@ -456,7 +466,7 @@
},
"confirm_unblock_user": {
"title": "Odblokovat účet",
- "message": "Confirm to unblock %s"
+ "message": "Potvrďte odblokování %s"
},
"confirm_show_reblogs": {
"title": "Show Reblogs",
@@ -468,235 +478,235 @@
}
},
"accessibility": {
- "show_avatar_image": "Show avatar image",
- "edit_avatar_image": "Edit avatar image",
- "show_banner_image": "Show banner image",
- "double_tap_to_open_the_list": "Double tap to open the list"
+ "show_avatar_image": "Zobrazit obrázek avataru",
+ "edit_avatar_image": "Upravit obrázek avataru",
+ "show_banner_image": "Zobrazit obrázek banneru",
+ "double_tap_to_open_the_list": "Dvojitým poklepáním otevřete seznam"
}
},
"follower": {
- "title": "follower",
- "footer": "Followers from other servers are not displayed."
+ "title": "sledující",
+ "footer": "Sledující z jiných serverů nejsou zobrazeni."
},
"following": {
- "title": "following",
- "footer": "Follows from other servers are not displayed."
+ "title": "sledování",
+ "footer": "Sledování z jiných serverů není zobrazeno."
},
"familiarFollowers": {
- "title": "Followers you familiar",
- "followed_by_names": "Followed by %s"
+ "title": "Sledující, které znáte",
+ "followed_by_names": "Sledován od %s"
},
"favorited_by": {
- "title": "Favorited By"
+ "title": "Oblíben"
},
"reblogged_by": {
"title": "Reblogged By"
},
"search": {
- "title": "Search",
+ "title": "Hledat",
"search_bar": {
- "placeholder": "Search hashtags and users",
- "cancel": "Cancel"
+ "placeholder": "Hledat hashtagy a uživatele",
+ "cancel": "Zrušit"
},
"recommend": {
- "button_text": "See All",
+ "button_text": "Zobrazit vše",
"hash_tag": {
- "title": "Trending on Mastodon",
- "description": "Hashtags that are getting quite a bit of attention",
- "people_talking": "%s people are talking"
+ "title": "Populární na Mastodonu",
+ "description": "Hashtagy, kterým se dostává dosti pozornosti",
+ "people_talking": "%s lidí mluví"
},
"accounts": {
- "title": "Accounts you might like",
- "description": "You may like to follow these accounts",
- "follow": "Follow"
+ "title": "Účty, které by se vám mohly líbit",
+ "description": "Možná budete chtít sledovat tyto účty",
+ "follow": "Sledovat"
}
},
"searching": {
"segment": {
- "all": "All",
- "people": "People",
- "hashtags": "Hashtags",
- "posts": "Posts"
+ "all": "Vše",
+ "people": "Lidé",
+ "hashtags": "Hashtagy",
+ "posts": "Příspěvky"
},
"empty_state": {
- "no_results": "No results"
+ "no_results": "Žádné výsledky"
},
- "recent_search": "Recent searches",
- "clear": "Clear"
+ "recent_search": "Nedávná hledání",
+ "clear": "Vymazat"
}
},
"discovery": {
"tabs": {
- "posts": "Posts",
- "hashtags": "Hashtags",
- "news": "News",
- "community": "Community",
- "for_you": "For You"
+ "posts": "Příspěvky",
+ "hashtags": "Hashtagy",
+ "news": "Zprávy",
+ "community": "Komunita",
+ "for_you": "Pro vás"
},
- "intro": "These are the posts gaining traction in your corner of Mastodon."
+ "intro": "Toto jsou příspěvky, které získávají pozornost ve vašem koutu Mastodonu."
},
"favorite": {
- "title": "Your Favorites"
+ "title": "Vaše oblíbené"
},
"notification": {
"title": {
- "Everything": "Everything",
- "Mentions": "Mentions"
+ "Everything": "Všechno",
+ "Mentions": "Zmínky"
},
"notification_description": {
- "followed_you": "followed you",
- "favorited_your_post": "favorited your post",
- "reblogged_your_post": "reblogged your post",
- "mentioned_you": "mentioned you",
- "request_to_follow_you": "request to follow you",
- "poll_has_ended": "poll has ended"
+ "followed_you": "vás sleduje",
+ "favorited_your_post": "si oblíbil váš příspěvek",
+ "reblogged_your_post": "boostnul váš příspěvek",
+ "mentioned_you": "vás zmínil/a",
+ "request_to_follow_you": "požádat vás o sledování",
+ "poll_has_ended": "anketa skončila"
},
"keyobard": {
- "show_everything": "Show Everything",
- "show_mentions": "Show Mentions"
+ "show_everything": "Zobrazit vše",
+ "show_mentions": "Zobrazit zmínky"
},
"follow_request": {
- "accept": "Accept",
- "accepted": "Accepted",
- "reject": "reject",
- "rejected": "Rejected"
+ "accept": "Přijmout",
+ "accepted": "Přijato",
+ "reject": "odmítnout",
+ "rejected": "Zamítnuto"
}
},
"thread": {
- "back_title": "Post",
- "title": "Post from %s"
+ "back_title": "Příspěvek",
+ "title": "Příspěvek od %s"
},
"settings": {
- "title": "Settings",
+ "title": "Nastavení",
"section": {
"appearance": {
- "title": "Appearance",
- "automatic": "Automatic",
- "light": "Always Light",
- "dark": "Always Dark"
+ "title": "Vzhled",
+ "automatic": "Automaticky",
+ "light": "Vždy světlý",
+ "dark": "Vždy tmavý"
},
"look_and_feel": {
- "title": "Look and Feel",
- "use_system": "Use System",
- "really_dark": "Really Dark",
+ "title": "Vzhled a chování",
+ "use_system": "Použít systém",
+ "really_dark": "Skutečně tmavý",
"sorta_dark": "Sorta Dark",
- "light": "Light"
+ "light": "Světlý"
},
"notifications": {
- "title": "Notifications",
- "favorites": "Favorites my post",
- "follows": "Follows me",
- "boosts": "Reblogs my post",
- "mentions": "Mentions me",
+ "title": "Upozornění",
+ "favorites": "Oblíbil si můj příspěvek",
+ "follows": "Sleduje mě",
+ "boosts": "Boostnul můj příspěvek",
+ "mentions": "Zmiňuje mě",
"trigger": {
- "anyone": "anyone",
- "follower": "a follower",
- "follow": "anyone I follow",
- "noone": "no one",
- "title": "Notify me when"
+ "anyone": "kdokoliv",
+ "follower": "sledující",
+ "follow": "kdokoli, koho sleduji",
+ "noone": "nikdo",
+ "title": "Upozornit, když"
}
},
"preference": {
- "title": "Preferences",
- "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",
- "open_links_in_mastodon": "Open links in Mastodon"
+ "title": "Předvolby",
+ "true_black_dark_mode": "Skutečný černý tmavý režim",
+ "disable_avatar_animation": "Zakázat animované avatary",
+ "disable_emoji_animation": "Zakázat animované emoji",
+ "using_default_browser": "Použít výchozí prohlížeč pro otevírání odkazů",
+ "open_links_in_mastodon": "Otevřít odkazy v Mastodonu"
},
"boring_zone": {
- "title": "The Boring Zone",
- "account_settings": "Account Settings",
- "terms": "Terms of Service",
- "privacy": "Privacy Policy"
+ "title": "Nudná část",
+ "account_settings": "Nastavení účtu",
+ "terms": "Podmínky služby",
+ "privacy": "Zásady ochrany osobních údajů"
},
"spicy_zone": {
- "title": "The Spicy Zone",
- "clear": "Clear Media Cache",
- "signout": "Sign Out"
+ "title": "Ostrá část",
+ "clear": "Vymazat mezipaměť médií",
+ "signout": "Odhlásit se"
}
},
"footer": {
- "mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
+ "mastodon_description": "Mastodon je open source software. Na GitHub můžete nahlásit problémy na %s (%s)"
},
"keyboard": {
- "close_settings_window": "Close Settings Window"
+ "close_settings_window": "Zavřít okno nastavení"
}
},
"report": {
- "title_report": "Report",
- "title": "Report %s",
- "step1": "Step 1 of 2",
- "step2": "Step 2 of 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?",
- "report_sent_title": "Thanks for reporting, we’ll look into this.",
- "send": "Send Report",
- "skip_to_send": "Send without comment",
- "text_placeholder": "Type or paste additional comments",
- "reported": "REPORTED",
+ "title_report": "Nahlásit",
+ "title": "Nahlásit %s",
+ "step1": "Krok 1 ze 2",
+ "step2": "Krok 2 ze 2",
+ "content1": "Existují nějaké další příspěvky, které byste chtěli přidat do zprávy?",
+ "content2": "Je o tomto hlášení něco, co by měli vědět moderátoři?",
+ "report_sent_title": "Děkujeme za nahlášení, podíváme se na to.",
+ "send": "Odeslat hlášení",
+ "skip_to_send": "Odeslat bez komentáře",
+ "text_placeholder": "Napište nebo vložte další komentáře",
+ "reported": "NAHLÁŠEN",
"step_one": {
- "step_1_of_4": "Step 1 of 4",
- "whats_wrong_with_this_post": "What's wrong with this post?",
- "whats_wrong_with_this_account": "What's wrong with this account?",
- "whats_wrong_with_this_username": "What's wrong with %s?",
- "select_the_best_match": "Select the best match",
- "i_dont_like_it": "I don’t like it",
- "it_is_not_something_you_want_to_see": "It is not something you want to see",
- "its_spam": "It’s spam",
- "malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies",
- "it_violates_server_rules": "It violates server rules",
- "you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules",
- "its_something_else": "It’s something else",
- "the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories"
+ "step_1_of_4": "Krok 1 ze 4",
+ "whats_wrong_with_this_post": "Co je na tomto příspěvku špatně?",
+ "whats_wrong_with_this_account": "Co je špatně s tímto účtem?",
+ "whats_wrong_with_this_username": "Co je špatně na %s?",
+ "select_the_best_match": "Vyberte nejbližší možnost",
+ "i_dont_like_it": "Nelíbí se mi",
+ "it_is_not_something_you_want_to_see": "Není to něco, co chcete vidět",
+ "its_spam": "Je to spam",
+ "malicious_links_fake_engagement_or_repetetive_replies": "Škodlivé odkazy, falešné zapojení nebo opakující se odpovědi",
+ "it_violates_server_rules": "Porušuje pravidla serveru",
+ "you_are_aware_that_it_breaks_specific_rules": "Máte za to, že porušuje konkrétní pravidla",
+ "its_something_else": "Jde o něco jiného",
+ "the_issue_does_not_fit_into_other_categories": "Problém neodpovídá ostatním kategoriím"
},
"step_two": {
- "step_2_of_4": "Step 2 of 4",
- "which_rules_are_being_violated": "Which rules are being violated?",
- "select_all_that_apply": "Select all that apply",
- "i_just_don’t_like_it": "I just don’t like it"
+ "step_2_of_4": "Krok 2 ze 4",
+ "which_rules_are_being_violated": "Jaká pravidla jsou porušována?",
+ "select_all_that_apply": "Vyberte všechna relevantní",
+ "i_just_don’t_like_it": "Jen se mi to nelíbí"
},
"step_three": {
- "step_3_of_4": "Step 3 of 4",
- "are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?",
- "select_all_that_apply": "Select all that apply"
+ "step_3_of_4": "Krok 3 ze 4",
+ "are_there_any_posts_that_back_up_this_report": "Existují příspěvky dokládající toto hlášení?",
+ "select_all_that_apply": "Vyberte všechna relevantní"
},
"step_four": {
- "step_4_of_4": "Step 4 of 4",
- "is_there_anything_else_we_should_know": "Is there anything else we should know?"
+ "step_4_of_4": "Krok 4 ze 4",
+ "is_there_anything_else_we_should_know": "Je ještě něco jiného, co bychom měli vědět?"
},
"step_final": {
- "dont_want_to_see_this": "Don’t want to see this?",
- "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.",
- "unfollow": "Unfollow",
- "unfollowed": "Unfollowed",
- "unfollow_user": "Unfollow %s",
- "mute_user": "Mute %s",
- "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.",
- "block_user": "Block %s",
- "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.",
- "while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s"
+ "dont_want_to_see_this": "Nechcete to vidět?",
+ "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Když uvidíte něco, co se vám nelíbí na Mastodonu, můžete odstranit tuto osobu ze svého zážitku.",
+ "unfollow": "Přestat sledovat",
+ "unfollowed": "Už nesledujete",
+ "unfollow_user": "Přestat sledovat %s",
+ "mute_user": "Skrýt %s",
+ "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Neuvidíte jejich příspěvky nebo boostnutí v domovském kanálu. Nebudou vědět, že jsou skrytí.",
+ "block_user": "Blokovat %s",
+ "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Už nebudou moci sledovat nebo vidět vaše příspěvky, ale mohou vidět, zda byly zablokovány.",
+ "while_we_review_this_you_can_take_action_against_user": "Zatímco to posuzujeme, můžete podniknout kroky proti %s"
}
},
"preview": {
"keyboard": {
- "close_preview": "Close Preview",
- "show_next": "Show Next",
- "show_previous": "Show Previous"
+ "close_preview": "Zavřít náhled",
+ "show_next": "Zobrazit další",
+ "show_previous": "Zobrazit předchozí"
}
},
"account_list": {
- "tab_bar_hint": "Current selected profile: %s. Double tap then hold to show account switcher",
- "dismiss_account_switcher": "Dismiss Account Switcher",
- "add_account": "Add Account"
+ "tab_bar_hint": "Aktuální vybraný profil: %s. Dvojitým poklepáním zobrazíte přepínač účtů",
+ "dismiss_account_switcher": "Zrušit přepínač účtů",
+ "add_account": "Přidat účet"
},
"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": "Nový v Mastodonu",
+ "multiple_account_switch_intro_description": "Přepínání mezi více účty podržením tlačítka profilu.",
+ "accessibility_hint": "Dvojitým poklepáním tohoto průvodce odmítnete"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Záložky"
}
}
}
diff --git a/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json
index c6db73de0..88bbb346a 100644
--- a/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json
+++ b/Localization/StringsConvertor/input/cs.lproj/ios-infoPlist.json
@@ -1,6 +1,6 @@
{
- "NSCameraUsageDescription": "Used to take photo for post status",
- "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library",
- "NewPostShortcutItemTitle": "New Post",
- "SearchShortcutItemTitle": "Search"
+ "NSCameraUsageDescription": "Slouží k pořízení fotografie pro příspěvek",
+ "NSPhotoLibraryAddUsageDescription": "Slouží k uložení fotografie do knihovny fotografií",
+ "NewPostShortcutItemTitle": "Nový příspěvek",
+ "SearchShortcutItemTitle": "Hledat"
}
diff --git a/Localization/StringsConvertor/input/cy.lproj/app.json b/Localization/StringsConvertor/input/cy.lproj/app.json
index bc7f75d96..f36fe7d16 100644
--- a/Localization/StringsConvertor/input/cy.lproj/app.json
+++ b/Localization/StringsConvertor/input/cy.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Hybwch",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/da.lproj/app.json b/Localization/StringsConvertor/input/da.lproj/app.json
index 80b0882d9..a6a971860 100644
--- a/Localization/StringsConvertor/input/da.lproj/app.json
+++ b/Localization/StringsConvertor/input/da.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict
index c6a8a4297..f60c6b0d7 100644
--- a/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/de.lproj/Localizable.stringsdict
@@ -248,9 +248,9 @@
NSStringFormatValueTypeKey
ld
one
- 1 Follower
+ 1 Folgender
other
- %ld Follower
+ %ld Folgende
date.year.left
diff --git a/Localization/StringsConvertor/input/de.lproj/app.json b/Localization/StringsConvertor/input/de.lproj/app.json
index 355bfcc1b..481f07a4a 100644
--- a/Localization/StringsConvertor/input/de.lproj/app.json
+++ b/Localization/StringsConvertor/input/de.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Abstimmen",
"closed": "Beendet"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Antworten",
"reblog": "Teilen",
@@ -376,7 +382,11 @@
"video": "Video",
"attachment_broken": "Dieses %s scheint defekt zu sein und\nkann nicht auf Mastodon hochgeladen werden.",
"description_photo": "Für Menschen mit Sehbehinderung beschreiben...",
- "description_video": "Für Menschen mit Sehbehinderung beschreiben..."
+ "description_video": "Für Menschen mit Sehbehinderung beschreiben...",
+ "load_failed": "Laden fehlgeschlagen",
+ "upload_failed": "Upload fehlgeschlagen",
+ "can_not_recognize_this_media_attachment": "Medienanhang wurde nicht erkannt",
+ "attachment_too_large": "Anhang zu groß"
},
"poll": {
"duration_time": "Dauer: %s",
@@ -425,7 +435,7 @@
"dashboard": {
"posts": "Beiträge",
"following": "Gefolgte",
- "followers": "Folger"
+ "followers": "Folgende"
},
"fields": {
"add_row": "Zeile hinzufügen",
@@ -460,7 +470,7 @@
},
"confirm_show_reblogs": {
"title": "Reblogs anzeigen",
- "message": "Confirm to show reblogs"
+ "message": "Bestätigen um Reblogs anzuzeigen"
},
"confirm_hide_reblogs": {
"title": "Reblogs ausblenden",
@@ -476,11 +486,11 @@
},
"follower": {
"title": "Follower",
- "footer": "Follower von anderen Servern werden nicht angezeigt."
+ "footer": "Folger, die nicht auf deinem Server registriert sind, werden nicht angezeigt."
},
"following": {
"title": "Folgende",
- "footer": "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt."
+ "footer": "Gefolgte, die nicht auf deinem Server registriert sind, werden nicht angezeigt."
},
"familiarFollowers": {
"title": "Follower, die dir bekannt vorkommen",
@@ -590,7 +600,7 @@
"mentions": "Mich erwähnt",
"trigger": {
"anyone": "jeder",
- "follower": "ein Folger",
+ "follower": "ein Folgender",
"follow": "ein von mir Gefolgter",
"noone": "niemand",
"title": "Benachrichtige mich, wenn"
@@ -696,7 +706,7 @@
"accessibility_hint": "Doppeltippen, um diesen Assistenten zu schließen"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Lesezeichen"
}
}
}
diff --git a/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json
index fe8fe1c1a..a571fba1c 100644
--- a/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json
+++ b/Localization/StringsConvertor/input/de.lproj/ios-infoPlist.json
@@ -1,6 +1,6 @@
{
- "NSCameraUsageDescription": "Verwendet um Fotos für neue Beiträge aufzunehmen",
- "NSPhotoLibraryAddUsageDescription": "Verwendet um Fotos zu speichern",
+ "NSCameraUsageDescription": "Wird verwendet, um Fotos für neue Beiträge aufzunehmen",
+ "NSPhotoLibraryAddUsageDescription": "Wird verwendet, um Foto in der Foto-Mediathek zu speichern",
"NewPostShortcutItemTitle": "Neuer Beitrag",
- "SearchShortcutItemTitle": "Suche"
+ "SearchShortcutItemTitle": "Suchen"
}
diff --git a/Localization/StringsConvertor/input/en-US.lproj/app.json b/Localization/StringsConvertor/input/en-US.lproj/app.json
index 80b0882d9..a6a971860 100644
--- a/Localization/StringsConvertor/input/en-US.lproj/app.json
+++ b/Localization/StringsConvertor/input/en-US.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict
index bdcae6ac9..297e6675a 100644
--- a/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/en.lproj/Localizable.stringsdict
@@ -50,6 +50,28 @@
%ld characters
+ a11y.plural.count.characters_left
+
+ NSStringLocalizedFormatKey
+ %#@character_count@ left
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
plural.count.followed_by_and_mutual
NSStringLocalizedFormatKey
diff --git a/Localization/StringsConvertor/input/en.lproj/app.json b/Localization/StringsConvertor/input/en.lproj/app.json
index ff2926996..7434a34a2 100644
--- a/Localization/StringsConvertor/input/en.lproj/app.json
+++ b/Localization/StringsConvertor/input/en.lproj/app.json
@@ -138,7 +138,7 @@
},
"meta_entity": {
"url": "Link: %s",
- "hashtag": "Hastag %s",
+ "hashtag": "Hashtag: %s",
"mention": "Show Profile: %s",
"email": "Email address: %s"
},
@@ -382,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
@@ -413,7 +417,9 @@
"custom_emoji_picker": "Custom Emoji Picker",
"enable_content_warning": "Enable Content Warning",
"disable_content_warning": "Disable Content Warning",
- "post_visibility_menu": "Post Visibility Menu"
+ "post_visibility_menu": "Post Visibility Menu",
+ "post_options": "Post Options",
+ "posting_as": "Posting as %s"
},
"keyboard": {
"discard_post": "Discard Post",
diff --git a/Localization/StringsConvertor/input/es-AR.lproj/app.json b/Localization/StringsConvertor/input/es-AR.lproj/app.json
index 62d439a3c..309cf4d34 100644
--- a/Localization/StringsConvertor/input/es-AR.lproj/app.json
+++ b/Localization/StringsConvertor/input/es-AR.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Votar",
"closed": "Cerrada"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Responder",
"reblog": "Adherir",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "Este archivo de %s está roto\ny no se puede subir a Mastodon.",
"description_photo": "Describí la imagen para personas con dificultades visuales…",
- "description_video": "Describí el video para personas con dificultades visuales…"
+ "description_video": "Describí el video para personas con dificultades visuales…",
+ "load_failed": "Falló la descarga",
+ "upload_failed": "Falló la subida",
+ "can_not_recognize_this_media_attachment": "No se pudo reconocer este archivo adjunto",
+ "attachment_too_large": "Adjunto demasiado grande"
},
"poll": {
"duration_time": "Duración: %s",
diff --git a/Localization/StringsConvertor/input/es.lproj/app.json b/Localization/StringsConvertor/input/es.lproj/app.json
index 39e0f37d1..7eaff340d 100644
--- a/Localization/StringsConvertor/input/es.lproj/app.json
+++ b/Localization/StringsConvertor/input/es.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vota",
"closed": "Cerrado"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Responder",
"reblog": "Rebloguear",
@@ -376,7 +382,11 @@
"video": "vídeo",
"attachment_broken": "Este %s está roto y no puede\nsubirse a Mastodon.",
"description_photo": "Describe la foto para los usuarios con dificultad visual...",
- "description_video": "Describe el vídeo para los usuarios con dificultad visual..."
+ "description_video": "Describe el vídeo para los usuarios con dificultad visual...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duración: %s",
diff --git a/Localization/StringsConvertor/input/eu.lproj/app.json b/Localization/StringsConvertor/input/eu.lproj/app.json
index 5c2e16601..3f58f522c 100644
--- a/Localization/StringsConvertor/input/eu.lproj/app.json
+++ b/Localization/StringsConvertor/input/eu.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Bozkatu",
"closed": "Itxita"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Erantzun",
"reblog": "Bultzada",
@@ -376,7 +382,11 @@
"video": "bideoa",
"attachment_broken": "%s hondatuta dago eta ezin da\nMastodonera igo.",
"description_photo": "Deskribatu argazkia ikusmen arazoak dituztenentzat...",
- "description_video": "Deskribatu bideoa ikusmen arazoak dituztenentzat..."
+ "description_video": "Deskribatu bideoa ikusmen arazoak dituztenentzat...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Iraupena: %s",
diff --git a/Localization/StringsConvertor/input/fi.lproj/app.json b/Localization/StringsConvertor/input/fi.lproj/app.json
index d6210c4d5..a42642786 100644
--- a/Localization/StringsConvertor/input/fi.lproj/app.json
+++ b/Localization/StringsConvertor/input/fi.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Suljettu"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Vastaa",
"reblog": "Jaa edelleen",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
"description_photo": "Kuvaile kuva näkövammaisille...",
- "description_video": "Kuvaile video näkövammaisille..."
+ "description_video": "Kuvaile video näkövammaisille...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Kesto: %s",
diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json
index ed53d1096..25bb6e511 100644
--- a/Localization/StringsConvertor/input/fr.lproj/app.json
+++ b/Localization/StringsConvertor/input/fr.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Voter",
"closed": "Fermé"
},
+ "meta_entity": {
+ "url": "Lien : %s",
+ "hashtag": "Hashtag : %s",
+ "mention": "Afficher le profile : %s",
+ "email": "Adresse e-mail : %s"
+ },
"actions": {
"reply": "Répondre",
"reblog": "Rebloguer",
@@ -376,7 +382,11 @@
"video": "vidéo",
"attachment_broken": "Ce %s est brisé et ne peut pas être\ntéléversé sur Mastodon.",
"description_photo": "Décrire cette photo pour les personnes malvoyantes...",
- "description_video": "Décrire cette vidéo pour les personnes malvoyantes..."
+ "description_video": "Décrire cette vidéo pour les personnes malvoyantes...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Durée: %s",
@@ -696,7 +706,7 @@
"accessibility_hint": "Tapotez deux fois pour fermer cet assistant"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Favoris"
}
}
}
diff --git a/Localization/StringsConvertor/input/gd.lproj/app.json b/Localization/StringsConvertor/input/gd.lproj/app.json
index a2062a89b..c1d17f813 100644
--- a/Localization/StringsConvertor/input/gd.lproj/app.json
+++ b/Localization/StringsConvertor/input/gd.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Cuir bhòt",
"closed": "Dùinte"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Freagair",
"reblog": "Brosnaich",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "Seo %s a tha briste is cha ghabh\na luchdadh suas gu Mastodon.",
"description_photo": "Mìnich an dealbh dhan fheadhainn air a bheil cion-lèirsinne…",
- "description_video": "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…"
+ "description_video": "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Faide: %s",
diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json
index 513573f79..3c394be95 100644
--- a/Localization/StringsConvertor/input/gl.lproj/app.json
+++ b/Localization/StringsConvertor/input/gl.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Votar",
"closed": "Pechada"
},
+ "meta_entity": {
+ "url": "Ligazón: %s",
+ "hashtag": "Cancelo: %s",
+ "mention": "Mostrar Perfil: %s",
+ "email": "Enderezo de email: %s"
+ },
"actions": {
"reply": "Responder",
"reblog": "Promover",
@@ -181,8 +187,8 @@
"unmute_user": "Deixar de acalar a @%s",
"muted": "Acalada",
"edit_info": "Editar info",
- "show_reblogs": "Show Reblogs",
- "hide_reblogs": "Hide Reblogs"
+ "show_reblogs": "Mostrar Promocións",
+ "hide_reblogs": "Agochar Promocións"
},
"timeline": {
"filtered": "Filtrado",
@@ -376,7 +382,11 @@
"video": "vídeo",
"attachment_broken": "Este %s está estragado e non pode\nser subido a Mastodon.",
"description_photo": "Describe a foto para persoas con problemas visuais...",
- "description_video": "Describe o vídeo para persoas con problemas visuais..."
+ "description_video": "Describe o vídeo para persoas con problemas visuais...",
+ "load_failed": "Fallou a carga",
+ "upload_failed": "Erro na subida",
+ "can_not_recognize_this_media_attachment": "Non se recoñece o tipo de multimedia",
+ "attachment_too_large": "Adxunto demasiado grande"
},
"poll": {
"duration_time": "Duración: %s",
@@ -459,12 +469,12 @@
"message": "Confirma o desbloqueo de %s"
},
"confirm_show_reblogs": {
- "title": "Show Reblogs",
- "message": "Confirm to show reblogs"
+ "title": "Mostrar Promocións",
+ "message": "Confirma para ver promocións"
},
"confirm_hide_reblogs": {
- "title": "Hide Reblogs",
- "message": "Confirm to hide reblogs"
+ "title": "Agochar Promocións",
+ "message": "Confirma para agochar promocións"
}
},
"accessibility": {
@@ -696,7 +706,7 @@
"accessibility_hint": "Dobre toque para desbotar este asistente"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Marcadores"
}
}
}
diff --git a/Localization/StringsConvertor/input/hi.lproj/app.json b/Localization/StringsConvertor/input/hi.lproj/app.json
index d9ef32b3a..f0fedf75f 100644
--- a/Localization/StringsConvertor/input/hi.lproj/app.json
+++ b/Localization/StringsConvertor/input/hi.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/id.lproj/app.json b/Localization/StringsConvertor/input/id.lproj/app.json
index 607e9a638..d942a22ad 100644
--- a/Localization/StringsConvertor/input/id.lproj/app.json
+++ b/Localization/StringsConvertor/input/id.lproj/app.json
@@ -6,29 +6,29 @@
"please_try_again_later": "Silakan coba lagi nanti."
},
"sign_up_failure": {
- "title": "Sign Up Failure"
+ "title": "Gagal Mendaftar"
},
"server_error": {
"title": "Kesalahan Server"
},
"vote_failure": {
- "title": "Vote Failure",
+ "title": "Gagal Voting",
"poll_ended": "Japat telah berakhir"
},
"discard_post_content": {
"title": "Hapus Draf",
- "message": "Confirm to discard composed post content."
+ "message": "Konfirmasi untuk mengabaikan postingan yang dibuat."
},
"publish_post_failure": {
- "title": "Publish Failure",
- "message": "Failed to publish the post.\nPlease check your internet connection.",
+ "title": "Gagal Mempublikasikan",
+ "message": "Gagal mempublikasikan postingan.\nMohon periksa koneksi Internet Anda.",
"attachments_message": {
"video_attach_with_photo": "Tidak dapat melampirkan video di postingan yang sudah mengandung gambar.",
"more_than_one_video": "Tidak dapat melampirkan lebih dari satu video."
}
},
"edit_profile_failure": {
- "title": "Edit Profile Error",
+ "title": "Masalah dalam mengubah profil",
"message": "Tidak dapat menyunting profil. Harap coba lagi."
},
"sign_out": {
@@ -37,16 +37,16 @@
"confirm": "Keluar"
},
"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.",
+ "title": "Apakah Anda benar, benar yakin ingin memblokir keseluruhan %s? Dalam kebanyakan kasus, beberapa pemblokiran atau pembisuan yang ditargetkan sudah cukup dan lebih disukai. Anda tidak akan melihat konten dari domain tersebut dan semua pengikut Anda dari domain itu akan dihapus.",
"block_entire_domain": "Blokir Domain"
},
"save_photo_failure": {
- "title": "Save Photo Failure",
- "message": "Please enable the photo library access permission to save the photo."
+ "title": "Gagal Menyimpan Foto",
+ "message": "Mohon aktifkan izin akses pustaka foto untuk menyimpan foto."
},
"delete_post": {
"title": "Apakah Anda yakin ingin menghapus postingan ini?",
- "message": "Are you sure you want to delete this post?"
+ "message": "Apakah Anda yakin untuk menghapus kiriman ini?"
},
"clean_cache": {
"title": "Bersihkan Cache",
@@ -67,11 +67,11 @@
"done": "Selesai",
"confirm": "Konfirmasi",
"continue": "Lanjut",
- "compose": "Compose",
+ "compose": "Tulis",
"cancel": "Batal",
- "discard": "Discard",
+ "discard": "Buang",
"try_again": "Coba Lagi",
- "take_photo": "Take Photo",
+ "take_photo": "Ambil Foto",
"save_photo": "Simpan Foto",
"copy_photo": "Salin Foto",
"sign_in": "Masuk",
@@ -82,9 +82,9 @@
"share_user": "Bagikan %s",
"share_post": "Bagikan Postingan",
"open_in_safari": "Buka di Safari",
- "open_in_browser": "Open in Browser",
+ "open_in_browser": "Buka di Peramban",
"find_people": "Cari orang untuk diikuti",
- "manually_search": "Manually search instead",
+ "manually_search": "Cari secara manual saja",
"skip": "Lewati",
"reply": "Balas",
"report_user": "Laporkan %s",
@@ -111,16 +111,16 @@
"next_status": "Postingan Selanjutnya",
"open_status": "Buka Postingan",
"open_author_profile": "Buka Profil Penulis",
- "open_reblogger_profile": "Open Reblogger's Profile",
+ "open_reblogger_profile": "Buka Profil Reblogger",
"reply_status": "Balas Postingan",
- "toggle_reblog": "Toggle Reblog on Post",
- "toggle_favorite": "Toggle Favorite on Post",
- "toggle_content_warning": "Toggle Content Warning",
- "preview_image": "Preview Image"
+ "toggle_reblog": "Nyalakan Reblog pada Postingan",
+ "toggle_favorite": "Nyalakan Favorit pada Postingan",
+ "toggle_content_warning": "Nyalakan Peringatan Konten",
+ "preview_image": "Pratinjau Gambar"
},
"segmented_control": {
- "previous_section": "Previous Section",
- "next_section": "Next Section"
+ "previous_section": "Bagian Sebelumnya",
+ "next_section": "Bagian Selanjutnya"
}
},
"status": {
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Ditutup"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Balas",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "%s ini rusak dan tidak dapat diunggah ke Mastodon.",
"description_photo": "Jelaskan fotonya untuk mereka yang tidak dapat melihat dengan jelas...",
- "description_video": "Jelaskan videonya untuk mereka yang tidak dapat melihat dengan jelas..."
+ "description_video": "Jelaskan videonya untuk mereka yang tidak dapat melihat dengan jelas...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Durasi: %s",
diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json
index 269d299ec..73f42d1eb 100644
--- a/Localization/StringsConvertor/input/it.lproj/app.json
+++ b/Localization/StringsConvertor/input/it.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vota",
"closed": "Chiuso"
},
+ "meta_entity": {
+ "url": "Collegamento: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Mostra il profilo: %s",
+ "email": "Indirizzo email: %s"
+ },
"actions": {
"reply": "Rispondi",
"reblog": "Condivisione",
@@ -376,7 +382,11 @@
"video": "filmato",
"attachment_broken": "Questo %s è rotto e non può essere\ncaricato su Mastodon.",
"description_photo": "Descrivi la foto per gli utenti ipovedenti...",
- "description_video": "Descrivi il filmato per gli utenti ipovedenti..."
+ "description_video": "Descrivi il filmato per gli utenti ipovedenti...",
+ "load_failed": "Caricamento fallito",
+ "upload_failed": "Caricamento fallito",
+ "can_not_recognize_this_media_attachment": "Impossibile riconoscere questo allegato multimediale",
+ "attachment_too_large": "Allegato troppo grande"
},
"poll": {
"duration_time": "Durata: %s",
@@ -696,7 +706,7 @@
"accessibility_hint": "Doppio tocco per eliminare questa procedura guidata"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Segnalibri"
}
}
}
diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json
index b7615abf3..9ff2a60a6 100644
--- a/Localization/StringsConvertor/input/ja.lproj/app.json
+++ b/Localization/StringsConvertor/input/ja.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "投票",
"closed": "終了"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "返信",
"reblog": "ブースト",
@@ -376,7 +382,11 @@
"video": "動画",
"attachment_broken": "%sは壊れていてMastodonにアップロードできません。",
"description_photo": "閲覧が難しいユーザーへの画像説明",
- "description_video": "閲覧が難しいユーザーへの映像説明"
+ "description_video": "閲覧が難しいユーザーへの映像説明",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "期間: %s",
diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json
index 2cff3d68d..9c5d7659a 100644
--- a/Localization/StringsConvertor/input/kab.lproj/app.json
+++ b/Localization/StringsConvertor/input/kab.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Dɣeṛ",
"closed": "Ifukk"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Err",
"reblog": "Aɛiwed n usuffeɣ",
@@ -376,7 +382,11 @@
"video": "tavidyutt",
"attachment_broken": "%s-a yerreẓ, ur yezmir ara\nAd d-yettwasali ɣef Mastodon.",
"description_photo": "Glem-d tawlaft i wid yesɛan ugur deg yiẓri...",
- "description_video": "Glem-d tavidyut i wid yesɛan ugur deg yiẓri..."
+ "description_video": "Glem-d tavidyut i wid yesɛan ugur deg yiẓri...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Tangazt: %s",
diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json
index d48edf3ae..098f514ee 100644
--- a/Localization/StringsConvertor/input/kmr.lproj/app.json
+++ b/Localization/StringsConvertor/input/kmr.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Deng bide",
"closed": "Girtî"
},
+ "meta_entity": {
+ "url": "Girêdan: %s",
+ "hashtag": "Hashtagê: %s",
+ "mention": "Profîlê nîşan bide: %s",
+ "email": "Navnîşanên e-nameyê: %s"
+ },
"actions": {
"reply": "Bersivê bide",
"reblog": "Ji nû ve nivîsandin",
@@ -181,8 +187,8 @@
"unmute_user": "%s bêdeng neke",
"muted": "Bêdengkirî",
"edit_info": "Zanyariyan serrast bike",
- "show_reblogs": "Show Reblogs",
- "hide_reblogs": "Hide Reblogs"
+ "show_reblogs": "Bilindkirinan nîşan bide",
+ "hide_reblogs": "Bilindkirinan veşêre"
},
"timeline": {
"filtered": "Parzûnkirî",
@@ -376,7 +382,11 @@
"video": "vîdyo",
"attachment_broken": "Ev %s naxebite û nayê barkirin\n li ser Mastodon.",
"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..."
+ "description_video": "Vîdyoyê ji bo kêmbînên dîtbar bide nasîn...",
+ "load_failed": "Barkirin têk çû",
+ "upload_failed": "Barkirin têk çû",
+ "can_not_recognize_this_media_attachment": "Nikare ev pêveka medyayê nas bike",
+ "attachment_too_large": "Pêvek pir mezin e"
},
"poll": {
"duration_time": "Dirêjî: %s",
@@ -459,12 +469,12 @@
"message": "Ji bo rakirina astengkirinê %s bipejirîne"
},
"confirm_show_reblogs": {
- "title": "Show Reblogs",
- "message": "Confirm to show reblogs"
+ "title": "Bilindkirinan nîşan bide",
+ "message": "Bo nîşandana bilindkirinan bipejirîne"
},
"confirm_hide_reblogs": {
- "title": "Hide Reblogs",
- "message": "Confirm to hide reblogs"
+ "title": "Bilindkirinan veşêre",
+ "message": "Bo veşartina bilindkirinan bipejirîne"
}
},
"accessibility": {
@@ -696,7 +706,7 @@
"accessibility_hint": "Du caran bitikîne da ku çarçoveyahilpekok ji holê rakî"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Şûnpel"
}
}
}
diff --git a/Localization/StringsConvertor/input/ko.lproj/app.json b/Localization/StringsConvertor/input/ko.lproj/app.json
index bbb4d1dea..826ac389c 100644
--- a/Localization/StringsConvertor/input/ko.lproj/app.json
+++ b/Localization/StringsConvertor/input/ko.lproj/app.json
@@ -129,13 +129,19 @@
"show_post": "게시물 보기",
"show_user_profile": "사용자 프로필 보기",
"content_warning": "열람 주의",
- "sensitive_content": "Sensitive Content",
+ "sensitive_content": "민감한 콘텐츠",
"media_content_warning": "아무 곳이나 눌러서 보기",
"tap_to_reveal": "눌러서 확인",
"poll": {
"vote": "투표",
"closed": "마감"
},
+ "meta_entity": {
+ "url": "링크: %s",
+ "hashtag": "해시태그: %s",
+ "mention": "프로필 보기: %s",
+ "email": "이메일 주소: %s"
+ },
"actions": {
"reply": "답글",
"reblog": "리블로그",
@@ -376,7 +382,11 @@
"video": "동영상",
"attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
"description_photo": "시각장애인을 위한 사진 설명…",
- "description_video": "시각장애인을 위한 영상 설명…"
+ "description_video": "시각장애인을 위한 영상 설명…",
+ "load_failed": "불러오기 실패",
+ "upload_failed": "업로드 실패",
+ "can_not_recognize_this_media_attachment": "이 미디어 첨부파일을 인식할 수 없습니다",
+ "attachment_too_large": "첨부파일이 너무 큽니다"
},
"poll": {
"duration_time": "기간: %s",
diff --git a/Localization/StringsConvertor/input/lv.lproj/app.json b/Localization/StringsConvertor/input/lv.lproj/app.json
index 0051383db..2835f0887 100644
--- a/Localization/StringsConvertor/input/lv.lproj/app.json
+++ b/Localization/StringsConvertor/input/lv.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Balsot",
"closed": "Aizvērts"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Atbildēt",
"reblog": "Reblogot",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/nl.lproj/app.json b/Localization/StringsConvertor/input/nl.lproj/app.json
index 649fe5064..e0b2872fb 100644
--- a/Localization/StringsConvertor/input/nl.lproj/app.json
+++ b/Localization/StringsConvertor/input/nl.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Stemmen",
"closed": "Gesloten"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reageren",
"reblog": "Delen",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "Deze %s is corrupt en kan niet geüpload worden naar Mastodon.",
"description_photo": "Omschrijf de foto voor mensen met een visuele beperking...",
- "description_video": "Omschrijf de video voor mensen met een visuele beperking..."
+ "description_video": "Omschrijf de video voor mensen met een visuele beperking...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duur: %s",
diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/app.json b/Localization/StringsConvertor/input/pt-BR.lproj/app.json
index 063ed346c..26e6edb76 100644
--- a/Localization/StringsConvertor/input/pt-BR.lproj/app.json
+++ b/Localization/StringsConvertor/input/pt-BR.lproj/app.json
@@ -6,7 +6,7 @@
"please_try_again_later": "Tente novamente mais tarde."
},
"sign_up_failure": {
- "title": "Sign Up Failure"
+ "title": "Falha no cadastro"
},
"server_error": {
"title": "Erro do servidor"
@@ -17,7 +17,7 @@
},
"discard_post_content": {
"title": "Deletar Rascunho",
- "message": "Confirm to discard composed post content."
+ "message": "Confirme para descartar o conteúdo da publicação composta."
},
"publish_post_failure": {
"title": "Falha ao publicar",
@@ -42,7 +42,7 @@
},
"save_photo_failure": {
"title": "Falha ao salvar foto",
- "message": "Please enable the photo library access permission to save the photo."
+ "message": "Por favor, ative a permissão de acesso à galeria para salvar a foto."
},
"delete_post": {
"title": "Deletar Toot",
@@ -71,212 +71,218 @@
"cancel": "Cancelar",
"discard": "Descartar",
"try_again": "Tente novamente",
- "take_photo": "Take Photo",
+ "take_photo": "Tirar foto",
"save_photo": "Salvar foto",
"copy_photo": "Copiar foto",
- "sign_in": "Sign In",
- "sign_up": "Sign Up",
- "see_more": "See More",
- "preview": "Preview",
+ "sign_in": "Entrar",
+ "sign_up": "Criar conta",
+ "see_more": "Ver mais",
+ "preview": "Pré-visualização",
"share": "Compartilhar",
"share_user": "Compartilhar %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",
- "reply": "Reply",
- "report_user": "Report %s",
- "block_domain": "Block %s",
- "unblock_domain": "Unblock %s",
- "settings": "Settings",
- "delete": "Delete"
+ "share_post": "Compartilhar postagem",
+ "open_in_safari": "Abrir no Safari",
+ "open_in_browser": "Abrir no navegador",
+ "find_people": "Encontre pessoas para seguir",
+ "manually_search": "Procure manualmente em vez disso",
+ "skip": "Pular",
+ "reply": "Responder",
+ "report_user": "Denunciar %s",
+ "block_domain": "Bloquear %s",
+ "unblock_domain": "Desbloquear %s",
+ "settings": "Configurações",
+ "delete": "Excluir"
},
"tabs": {
- "home": "Home",
- "search": "Search",
- "notification": "Notification",
- "profile": "Profile"
+ "home": "Início",
+ "search": "Buscar",
+ "notification": "Notificação",
+ "profile": "Perfil"
},
"keyboard": {
"common": {
- "switch_to_tab": "Switch to %s",
- "compose_new_post": "Compose New Post",
- "show_favorites": "Show Favorites",
- "open_settings": "Open Settings"
+ "switch_to_tab": "Mudar para %s",
+ "compose_new_post": "Compor novo toot",
+ "show_favorites": "Mostrar favoritos",
+ "open_settings": "Abrir configurações"
},
"timeline": {
- "previous_status": "Previous Post",
- "next_status": "Next Post",
- "open_status": "Open Post",
- "open_author_profile": "Open Author's Profile",
- "open_reblogger_profile": "Open Reblogger's Profile",
- "reply_status": "Reply to Post",
- "toggle_reblog": "Toggle Reblog on Post",
- "toggle_favorite": "Toggle Favorite on Post",
- "toggle_content_warning": "Toggle Content Warning",
- "preview_image": "Preview Image"
+ "previous_status": "Postagem anterior",
+ "next_status": "Próxima postagem",
+ "open_status": "Abrir toot",
+ "open_author_profile": "Abrir perfil do autor",
+ "open_reblogger_profile": "Abrir perfil do reblogger",
+ "reply_status": "Responder toot",
+ "toggle_reblog": "Ativar/desativar Reblog na postagem",
+ "toggle_favorite": "Ativar/desativar Favorito na postagem",
+ "toggle_content_warning": "Ativar/desativar Aviso de Conteúdo",
+ "preview_image": "Pré-visualizar imagem"
},
"segmented_control": {
- "previous_section": "Previous Section",
- "next_section": "Next Section"
+ "previous_section": "Seção anterior",
+ "next_section": "Próxima seção"
}
},
"status": {
- "user_reblogged": "%s reblogged",
- "user_replied_to": "Replied to %s",
- "show_post": "Show Post",
- "show_user_profile": "Show user profile",
- "content_warning": "Content Warning",
- "sensitive_content": "Sensitive Content",
- "media_content_warning": "Tap anywhere to reveal",
- "tap_to_reveal": "Tap to reveal",
+ "user_reblogged": "%s reblogou",
+ "user_replied_to": "Em resposta a %s",
+ "show_post": "Mostrar postagem",
+ "show_user_profile": "Mostrar perfil de usuário",
+ "content_warning": "Aviso de Conteúdo",
+ "sensitive_content": "Conteúdo sensível",
+ "media_content_warning": "Toque em qualquer lugar para revelar",
+ "tap_to_reveal": "Toque para revelar",
"poll": {
- "vote": "Vote",
- "closed": "Closed"
+ "vote": "Votar",
+ "closed": "Fechado"
+ },
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Mostrar perfil: %s",
+ "email": "Endereço de e-mail: %s"
},
"actions": {
"reply": "Responder",
- "reblog": "Reblog",
- "unreblog": "Undo reblog",
- "favorite": "Favorite",
- "unfavorite": "Unfavorite",
+ "reblog": "Reblogar",
+ "unreblog": "Desfazer reblog",
+ "favorite": "Favoritar",
+ "unfavorite": "Remover favorito",
"menu": "Menu",
- "hide": "Hide",
- "show_image": "Show image",
- "show_gif": "Show GIF",
- "show_video_player": "Show video player",
- "tap_then_hold_to_show_menu": "Tap then hold to show menu"
+ "hide": "Ocultar",
+ "show_image": "Exibir imagem",
+ "show_gif": "Exibir GIF",
+ "show_video_player": "Mostrar reprodutor de vídeo",
+ "tap_then_hold_to_show_menu": "Toque e em seguida segure para exibir o menu"
},
"tag": {
"url": "URL",
- "mention": "Mention",
+ "mention": "Mencionar",
"link": "Link",
"hashtag": "Hashtag",
- "email": "Email",
+ "email": "E-mail",
"emoji": "Emoji"
},
"visibility": {
"unlisted": "Everyone can see this post but not display in the public timeline.",
- "private": "Only their followers can see this post.",
- "private_from_me": "Only my followers can see this post.",
- "direct": "Only mentioned user can see this post."
+ "private": "Somente seus seguidores podem ver essa postagem.",
+ "private_from_me": "Somente meus seguidores podem ver essa postagem.",
+ "direct": "Somente o usuário mencionado pode ver essa postagem."
}
},
"friendship": {
- "follow": "Follow",
- "following": "Following",
- "request": "Request",
- "pending": "Pending",
- "block": "Block",
- "block_user": "Block %s",
- "block_domain": "Block %s",
- "unblock": "Unblock",
- "unblock_user": "Unblock %s",
- "blocked": "Blocked",
- "mute": "Mute",
- "mute_user": "Mute %s",
- "unmute": "Unmute",
- "unmute_user": "Unmute %s",
- "muted": "Muted",
- "edit_info": "Edit Info",
- "show_reblogs": "Show Reblogs",
- "hide_reblogs": "Hide Reblogs"
+ "follow": "Seguir",
+ "following": "Seguindo",
+ "request": "Solicitação",
+ "pending": "Pendente",
+ "block": "Bloquear",
+ "block_user": "Bloquear %s",
+ "block_domain": "Bloquear %s",
+ "unblock": "Desbloquear",
+ "unblock_user": "Desbloquear %s",
+ "blocked": "Bloqueado",
+ "mute": "Silenciar",
+ "mute_user": "Silenciar %s",
+ "unmute": "Remover silenciado",
+ "unmute_user": "Remover silenciado %s",
+ "muted": "Silenciado",
+ "edit_info": "Editar informação",
+ "show_reblogs": "Mostrar Reblogs",
+ "hide_reblogs": "Ocultar Reblogs"
},
"timeline": {
- "filtered": "Filtered",
+ "filtered": "Filtrado",
"timestamp": {
- "now": "Now"
+ "now": "Agora"
},
"loader": {
- "load_missing_posts": "Load missing posts",
- "loading_missing_posts": "Loading missing posts...",
- "show_more_replies": "Show more replies"
+ "load_missing_posts": "Carregar postagens em falta",
+ "loading_missing_posts": "Carregando postagens em falta...",
+ "show_more_replies": "Exibir mais respostas"
},
"header": {
- "no_status_found": "No Post Found",
+ "no_status_found": "Nenhuma postagem encontrada",
"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."
+ "blocked_warning": "Você não pode ver o perfil desse usuário até que ele o desbloqueie.",
+ "user_blocked_warning": "Você não pode ver o perfil de %s até que ele o desbloqueie.",
+ "suspended_warning": "Esse usuário foi suspenso.",
+ "user_suspended_warning": "A conta de %s foi suspensa."
}
}
}
},
"scene": {
"welcome": {
- "slogan": "Social networking\nback in your hands.",
- "get_started": "Get Started",
- "log_in": "Log In"
+ "slogan": "Você no controle de sua rede social.",
+ "get_started": "Comece já",
+ "log_in": "Entrar"
},
"server_picker": {
- "title": "Mastodon is made of users in different servers.",
- "subtitle": "Pick a server based on your interests, region, or a general purpose one.",
- "subtitle_extend": "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.",
+ "title": "Mastodon é feito de usuários em instâncias diferentes.",
+ "subtitle": "Escolha uma instância baseada nos seus interesses, região, ou em uma proposta geral.",
+ "subtitle_extend": "Escolha uma instância baseada nos seus interesses, região, ou em uma proposta geral. Cada instância é operada por um indivíduo ou uma organização totalmente independente.",
"button": {
"category": {
- "all": "All",
- "all_accessiblity_description": "Category: All",
- "academia": "academia",
- "activism": "activism",
- "food": "food",
+ "all": "Todos",
+ "all_accessiblity_description": "Categoria: Todos",
+ "academia": "acadêmico",
+ "activism": "ativismo",
+ "food": "comida",
"furry": "furry",
- "games": "games",
- "general": "general",
- "journalism": "journalism",
+ "games": "jogos",
+ "general": "geral",
+ "journalism": "jornalismo",
"lgbt": "lgbt",
"regional": "regional",
- "art": "art",
- "music": "music",
- "tech": "tech"
+ "art": "arte",
+ "music": "música",
+ "tech": "tecnologia"
},
- "see_less": "See Less",
- "see_more": "See More"
+ "see_less": "Ver menos",
+ "see_more": "Ver mais"
},
"label": {
- "language": "LANGUAGE",
- "users": "USERS",
- "category": "CATEGORY"
+ "language": "Idioma",
+ "users": "Usuários",
+ "category": "Categoria"
},
"input": {
- "placeholder": "Search servers",
- "search_servers_or_enter_url": "Search servers or enter URL"
+ "placeholder": "Procurar instâncias",
+ "search_servers_or_enter_url": "Procurar instâncias ou inserir URL"
},
"empty_state": {
- "finding_servers": "Finding available servers...",
- "bad_network": "Something went wrong while loading the data. Check your internet connection.",
- "no_results": "No results"
+ "finding_servers": "Procurando instâncias disponíveis...",
+ "bad_network": "Algo deu errado ao carregar os dados. Verifique sua conexão com a internet.",
+ "no_results": "Sem resultados"
}
},
"register": {
- "title": "Let’s get you set up on %s",
+ "title": "Vamos configurar você em %s",
"lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
"input": {
"avatar": {
- "delete": "Delete"
+ "delete": "Excluir"
},
"username": {
- "placeholder": "username",
- "duplicate_prompt": "This username is taken."
+ "placeholder": "nome de usuário",
+ "duplicate_prompt": "Esse nome de usuário já está sendo usado."
},
"display_name": {
- "placeholder": "display name"
+ "placeholder": "nome de exibição"
},
"email": {
- "placeholder": "email"
+ "placeholder": "e-mail"
},
"password": {
- "placeholder": "password",
- "require": "Your password needs at least:",
- "character_limit": "8 characters",
+ "placeholder": "senha",
+ "require": "Sua senha deve ter pelo menos:",
+ "character_limit": "8 carácteres",
"accessibility": {
"checked": "checked",
"unchecked": "unchecked"
},
- "hint": "Your password needs at least eight characters"
+ "hint": "Sua senha precisa ter pelo menos oito carácteres"
},
"invite": {
"registration_user_invite_request": "Why do you want to join?"
@@ -284,17 +290,17 @@
},
"error": {
"item": {
- "username": "Username",
- "email": "Email",
- "password": "Password",
- "agreement": "Agreement",
- "locale": "Locale",
- "reason": "Reason"
+ "username": "Nome de usuário",
+ "email": "E-mail",
+ "password": "Senha",
+ "agreement": "Termos de uso",
+ "locale": "Localidade",
+ "reason": "Motivo"
},
"reason": {
"blocked": "%s contains a disallowed email provider",
- "unreachable": "%s does not seem to exist",
- "taken": "%s is already in use",
+ "unreachable": "%s parece não existir",
+ "taken": "%s já está em uso",
"reserved": "%s is a reserved keyword",
"accepted": "%s must be accepted",
"blank": "%s is required",
@@ -366,27 +372,31 @@
"media_selection": {
"camera": "Take Photo",
"photo_library": "Photo Library",
- "browse": "Browse"
+ "browse": "Navegar"
},
- "content_input_placeholder": "Type or paste what’s on your mind",
- "compose_action": "Publish",
- "replying_to_user": "replying to %s",
+ "content_input_placeholder": "Digite ou cole o que está na sua mente",
+ "compose_action": "Publicar",
+ "replying_to_user": "em resposta a %s",
"attachment": {
- "photo": "photo",
- "video": "video",
+ "photo": "foto",
+ "video": "vídeo",
"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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
- "duration_time": "Duration: %s",
- "thirty_minutes": "30 minutes",
- "one_hour": "1 Hour",
- "six_hours": "6 Hours",
- "one_day": "1 Day",
- "three_days": "3 Days",
- "seven_days": "7 Days",
- "option_number": "Option %ld"
+ "duration_time": "Duração: %s",
+ "thirty_minutes": "30 minutos",
+ "one_hour": "1 hora",
+ "six_hours": "6 horas",
+ "one_day": "1 dia",
+ "three_days": "3 dias",
+ "seven_days": "7 dias",
+ "option_number": "Opção %ld"
},
"content_warning": {
"placeholder": "Write an accurate warning here..."
@@ -423,9 +433,9 @@
"follows_you": "Follows You"
},
"dashboard": {
- "posts": "posts",
- "following": "following",
- "followers": "followers"
+ "posts": "toots",
+ "following": "seguindo",
+ "followers": "seguidores"
},
"fields": {
"add_row": "Add Row",
@@ -508,30 +518,30 @@
"accounts": {
"title": "Accounts you might like",
"description": "You may like to follow these accounts",
- "follow": "Follow"
+ "follow": "Seguir"
}
},
"searching": {
"segment": {
- "all": "All",
- "people": "People",
+ "all": "Todos",
+ "people": "Pessoas",
"hashtags": "Hashtags",
- "posts": "Posts"
+ "posts": "Toots"
},
"empty_state": {
- "no_results": "No results"
+ "no_results": "Sem resultados"
},
- "recent_search": "Recent searches",
- "clear": "Clear"
+ "recent_search": "Pesquisas recentes",
+ "clear": "Limpar"
}
},
"discovery": {
"tabs": {
- "posts": "Posts",
+ "posts": "Toots",
"hashtags": "Hashtags",
- "news": "News",
- "community": "Community",
- "for_you": "For You"
+ "news": "Notícias",
+ "community": "Comunidade",
+ "for_you": "Para você"
},
"intro": "These are the posts gaining traction in your corner of Mastodon."
},
diff --git a/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json b/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json
index c6db73de0..04b53a160 100644
--- a/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json
+++ b/Localization/StringsConvertor/input/pt-BR.lproj/ios-infoPlist.json
@@ -1,6 +1,6 @@
{
- "NSCameraUsageDescription": "Used to take photo for post status",
- "NSPhotoLibraryAddUsageDescription": "Used to save photo into the Photo Library",
- "NewPostShortcutItemTitle": "New Post",
- "SearchShortcutItemTitle": "Search"
+ "NSCameraUsageDescription": "Usado para tirar uma foto para postagem",
+ "NSPhotoLibraryAddUsageDescription": "Usado para salvar foto na Galeria",
+ "NewPostShortcutItemTitle": "Novo Toot",
+ "SearchShortcutItemTitle": "Buscar"
}
diff --git a/Localization/StringsConvertor/input/pt.lproj/app.json b/Localization/StringsConvertor/input/pt.lproj/app.json
index 80b0882d9..a6a971860 100644
--- a/Localization/StringsConvertor/input/pt.lproj/app.json
+++ b/Localization/StringsConvertor/input/pt.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/ro.lproj/app.json b/Localization/StringsConvertor/input/ro.lproj/app.json
index 8b9da0903..a9d3804fa 100644
--- a/Localization/StringsConvertor/input/ro.lproj/app.json
+++ b/Localization/StringsConvertor/input/ro.lproj/app.json
@@ -32,9 +32,9 @@
"message": "Nu se poate edita profilul. Vă rugăm să încercaţi din nou."
},
"sign_out": {
- "title": "Deconectați-vă",
+ "title": "Deconectare",
"message": "Sigur doriți să vă deconectați?",
- "confirm": "Deconectați-vă"
+ "confirm": "Deconectare"
},
"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.",
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/ru.lproj/app.json b/Localization/StringsConvertor/input/ru.lproj/app.json
index 7a4833554..798cdb4c5 100644
--- a/Localization/StringsConvertor/input/ru.lproj/app.json
+++ b/Localization/StringsConvertor/input/ru.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Проголосовать",
"closed": "Завершён"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Ответить",
"reblog": "Продвинуть",
@@ -376,7 +382,11 @@
"video": "видео",
"attachment_broken": "Это %s повреждено и не может\nбыть отправлено в Mastodon.",
"description_photo": "Опишите фото для людей с нарушениями зрения...",
- "description_video": "Опишите видео для людей с нарушениями зрения..."
+ "description_video": "Опишите видео для людей с нарушениями зрения...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Продолжительность: %s",
diff --git a/Localization/StringsConvertor/input/si.lproj/app.json b/Localization/StringsConvertor/input/si.lproj/app.json
index f42e91ae1..816536440 100644
--- a/Localization/StringsConvertor/input/si.lproj/app.json
+++ b/Localization/StringsConvertor/input/si.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "ඡන්දය",
"closed": "වසා ඇත"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "පිළිතුරු",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/sl.lproj/app.json b/Localization/StringsConvertor/input/sl.lproj/app.json
index 99a823feb..37b62a45d 100644
--- a/Localization/StringsConvertor/input/sl.lproj/app.json
+++ b/Localization/StringsConvertor/input/sl.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Glasuj",
"closed": "Zaprto"
},
+ "meta_entity": {
+ "url": "Povezava: %s",
+ "hashtag": "Ključnik: %s",
+ "mention": "Pokaži profil: %s",
+ "email": "E-naslov: %s"
+ },
"actions": {
"reply": "Odgovori",
"reblog": "Poobjavi",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "To %s je okvarjeno in ga ni\nmožno naložiti v Mastodon.",
"description_photo": "Opiši fotografijo za slabovidne in osebe z okvaro vida ...",
- "description_video": "Opiši video za slabovidne in osebe z okvaro vida ..."
+ "description_video": "Opiši video za slabovidne in osebe z okvaro vida ...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Trajanje: %s",
@@ -696,7 +706,7 @@
"accessibility_hint": "Dvakrat tapnite, da zapustite tega čarovnika"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Zaznamki"
}
}
}
diff --git a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict
index 048af4732..c7317903d 100644
--- a/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict
+++ b/Localization/StringsConvertor/input/sv.lproj/Localizable.stringsdict
@@ -152,9 +152,9 @@
NSStringFormatValueTypeKey
ld
one
- %ld puff
+ %ld boost
other
- %ld puffar
+ %ld boostar
plural.count.reply
diff --git a/Localization/StringsConvertor/input/sv.lproj/app.json b/Localization/StringsConvertor/input/sv.lproj/app.json
index 85f243b03..c740609c9 100644
--- a/Localization/StringsConvertor/input/sv.lproj/app.json
+++ b/Localization/StringsConvertor/input/sv.lproj/app.json
@@ -111,9 +111,9 @@
"next_status": "Nästa inlägg",
"open_status": "Öppna inlägg",
"open_author_profile": "Öppna författarens profil",
- "open_reblogger_profile": "Öppna ompostarens profil",
+ "open_reblogger_profile": "Öppna boostarens profil",
"reply_status": "Svara på inlägg",
- "toggle_reblog": "Växla puff på inlägg",
+ "toggle_reblog": "Växla boost på inlägg",
"toggle_favorite": "Växla favorit på inlägg",
"toggle_content_warning": "Växla innehållsvarning",
"preview_image": "Förhandsgranska bild"
@@ -124,7 +124,7 @@
}
},
"status": {
- "user_reblogged": "%s puffade",
+ "user_reblogged": "%s boostade",
"user_replied_to": "Svarade på %s",
"show_post": "Visa inlägg",
"show_user_profile": "Visa användarprofil",
@@ -136,10 +136,16 @@
"vote": "Rösta",
"closed": "Stängd"
},
+ "meta_entity": {
+ "url": "Länk: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Visa profil: %s",
+ "email": "E-postadress: %s"
+ },
"actions": {
"reply": "Svara",
- "reblog": "Puffa",
- "unreblog": "Ångra puff",
+ "reblog": "Boosta",
+ "unreblog": "Ångra boost",
"favorite": "Favorit",
"unfavorite": "Ta bort favorit",
"menu": "Meny",
@@ -181,8 +187,8 @@
"unmute_user": "Avtysta %s",
"muted": "Tystad",
"edit_info": "Redigera info",
- "show_reblogs": "Visa knuffar",
- "hide_reblogs": "Dölj puffar"
+ "show_reblogs": "Visa boostar",
+ "hide_reblogs": "Dölj boostar"
},
"timeline": {
"filtered": "Filtrerat",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "Denna %s är trasig och kan inte\nladdas upp till Mastodon.",
"description_photo": "Beskriv fotot för synskadade...",
- "description_video": "Beskriv videon för de synskadade..."
+ "description_video": "Beskriv videon för de synskadade...",
+ "load_failed": "Det gick inte att läsa in",
+ "upload_failed": "Uppladdning misslyckades",
+ "can_not_recognize_this_media_attachment": "Känner inte igen mediebilagan",
+ "attachment_too_large": "Bilagan är för stor"
},
"poll": {
"duration_time": "Längd: %s",
@@ -459,12 +469,12 @@
"message": "Bekräfta för att avblockera %s"
},
"confirm_show_reblogs": {
- "title": "Visa puffar",
- "message": "Bekräfta för att visa puffar"
+ "title": "Visa boostar",
+ "message": "Bekräfta för att visa boostar"
},
"confirm_hide_reblogs": {
- "title": "Dölj puffar",
- "message": "Bekräfta för att dölja puffar"
+ "title": "Dölj boostar",
+ "message": "Bekräfta för att dölja boostar"
}
},
"accessibility": {
@@ -490,7 +500,7 @@
"title": "Favoriserad av"
},
"reblogged_by": {
- "title": "Puffat av"
+ "title": "Boostat av"
},
"search": {
"title": "Sök",
@@ -546,7 +556,7 @@
"notification_description": {
"followed_you": "följde dig",
"favorited_your_post": "favoriserade ditt inlägg",
- "reblogged_your_post": "puffade ditt inlägg",
+ "reblogged_your_post": "boostade ditt inlägg",
"mentioned_you": "nämnde dig",
"request_to_follow_you": "begär att följa dig",
"poll_has_ended": "omröstningen har avslutats"
@@ -586,7 +596,7 @@
"title": "Notiser",
"favorites": "Favoriserar mitt inlägg",
"follows": "Följer mig",
- "boosts": "Ompostar mitt inlägg",
+ "boosts": "Boostar mitt inlägg",
"mentions": "Nämner mig",
"trigger": {
"anyone": "alla",
@@ -672,7 +682,7 @@
"unfollowed": "Slutade följa",
"unfollow_user": "Avfölj %s",
"mute_user": "Tysta %s",
- "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Du kommer inte att se deras inlägg eller ompostningar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.",
+ "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Du kommer inte att se deras inlägg eller boostar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.",
"block_user": "Blockera %s",
"they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "De kommer inte längre att kunna följa eller se dina inlägg, men de kan se om de har blockerats.",
"while_we_review_this_you_can_take_action_against_user": "Medan vi granskar detta kan du vidta åtgärder mot %s"
@@ -696,7 +706,7 @@
"accessibility_hint": "Dubbeltryck för att avvisa den här guiden"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Bokmärken"
}
}
}
diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json
index 763b827cd..9b4316025 100644
--- a/Localization/StringsConvertor/input/th.lproj/app.json
+++ b/Localization/StringsConvertor/input/th.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "ลงคะแนน",
"closed": "ปิดแล้ว"
},
+ "meta_entity": {
+ "url": "ลิงก์: %s",
+ "hashtag": "แฮชแท็ก: %s",
+ "mention": "โปรไฟล์ที่แสดง: %s",
+ "email": "ที่อยู่อีเมล: %s"
+ },
"actions": {
"reply": "ตอบกลับ",
"reblog": "ดัน",
@@ -181,8 +187,8 @@
"unmute_user": "เลิกซ่อน %s",
"muted": "ซ่อนอยู่",
"edit_info": "แก้ไขข้อมูล",
- "show_reblogs": "Show Reblogs",
- "hide_reblogs": "Hide Reblogs"
+ "show_reblogs": "แสดงการดัน",
+ "hide_reblogs": "ซ่อนการดัน"
},
"timeline": {
"filtered": "กรองอยู่",
@@ -215,7 +221,7 @@
"server_picker": {
"title": "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ",
"subtitle": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ",
- "subtitle_extend": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ดำเนินการโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง",
+ "subtitle_extend": "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง",
"button": {
"category": {
"all": "ทั้งหมด",
@@ -376,7 +382,11 @@
"video": "วิดีโอ",
"attachment_broken": "%s นี้เสียหายและไม่สามารถ\nอัปโหลดไปยัง Mastodon",
"description_photo": "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น...",
- "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น..."
+ "description_video": "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น...",
+ "load_failed": "การโหลดล้มเหลว",
+ "upload_failed": "การอัปโหลดล้มเหลว",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "ไฟล์แนบใหญ่เกินไป"
},
"poll": {
"duration_time": "ระยะเวลา: %s",
@@ -459,12 +469,12 @@
"message": "ยืนยันเพื่อเลิกปิดกั้น %s"
},
"confirm_show_reblogs": {
- "title": "Show Reblogs",
- "message": "Confirm to show reblogs"
+ "title": "แสดงการดัน",
+ "message": "ยืนยันเพื่อแสดงการดัน"
},
"confirm_hide_reblogs": {
- "title": "Hide Reblogs",
- "message": "Confirm to hide reblogs"
+ "title": "ซ่อนการดัน",
+ "message": "ยืนยันเพื่อซ่อนการดัน"
}
},
"accessibility": {
@@ -696,7 +706,7 @@
"accessibility_hint": "แตะสองครั้งเพื่อปิดตัวช่วยสร้างนี้"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "ที่คั่นหน้า"
}
}
}
diff --git a/Localization/StringsConvertor/input/tr.lproj/app.json b/Localization/StringsConvertor/input/tr.lproj/app.json
index cef7fd7f4..2abb92845 100644
--- a/Localization/StringsConvertor/input/tr.lproj/app.json
+++ b/Localization/StringsConvertor/input/tr.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Oy ver",
"closed": "Kapandı"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Yanıtla",
"reblog": "Yeniden paylaş",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "Bu %s bozuk ve Mastodon'a\nyüklenemiyor.",
"description_photo": "Görme engelliler için fotoğrafı tarif edin...",
- "description_video": "Görme engelliler için videoyu tarif edin..."
+ "description_video": "Görme engelliler için videoyu tarif edin...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Süre: %s",
diff --git a/Localization/StringsConvertor/input/uk.lproj/app.json b/Localization/StringsConvertor/input/uk.lproj/app.json
index 80b0882d9..a6a971860 100644
--- a/Localization/StringsConvertor/input/uk.lproj/app.json
+++ b/Localization/StringsConvertor/input/uk.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Vote",
"closed": "Closed"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "Reply",
"reblog": "Reblog",
@@ -376,7 +382,11 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Duration: %s",
diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json
index b857399b3..5b7696727 100644
--- a/Localization/StringsConvertor/input/vi.lproj/app.json
+++ b/Localization/StringsConvertor/input/vi.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "Bình chọn",
"closed": "Kết thúc"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Hiện hồ sơ: %s",
+ "email": "Email: %s"
+ },
"actions": {
"reply": "Trả lời",
"reblog": "Đăng lại",
@@ -376,7 +382,11 @@
"video": "video",
"attachment_broken": "%s này bị lỗi và không thể\ntải lên Mastodon.",
"description_photo": "Mô tả hình ảnh cho người khiếm thị...",
- "description_video": "Mô tả video cho người khiếm thị..."
+ "description_video": "Mô tả video cho người khiếm thị...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "Thời hạn: %s",
@@ -696,7 +706,7 @@
"accessibility_hint": "Nhấn hai lần để bỏ qua"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "Tút đã lưu"
}
}
}
diff --git a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json
index 7f3703b8a..ddf89e159 100644
--- a/Localization/StringsConvertor/input/zh-Hans.lproj/app.json
+++ b/Localization/StringsConvertor/input/zh-Hans.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "投票",
"closed": "已关闭"
},
+ "meta_entity": {
+ "url": "Link: %s",
+ "hashtag": "Hashtag: %s",
+ "mention": "Show Profile: %s",
+ "email": "Email address: %s"
+ },
"actions": {
"reply": "回复",
"reblog": "转发",
@@ -376,7 +382,11 @@
"video": "视频",
"attachment_broken": "%s已损坏\n无法上传到 Mastodon",
"description_photo": "为视觉障碍人士添加照片的文字说明...",
- "description_video": "为视觉障碍人士添加视频的文字说明..."
+ "description_video": "为视觉障碍人士添加视频的文字说明...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "时长:%s",
diff --git a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json
index ae497109e..ab7343c99 100644
--- a/Localization/StringsConvertor/input/zh-Hant.lproj/app.json
+++ b/Localization/StringsConvertor/input/zh-Hant.lproj/app.json
@@ -136,6 +136,12 @@
"vote": "投票",
"closed": "已關閉"
},
+ "meta_entity": {
+ "url": "連結:%s",
+ "hashtag": "主題標籤: %s",
+ "mention": "顯示個人檔案:%s",
+ "email": "電子郵件地址:%s"
+ },
"actions": {
"reply": "回覆",
"reblog": "轉嘟",
@@ -376,7 +382,11 @@
"video": "影片",
"attachment_broken": "此 %s 已損毀,並無法被上傳至 Mastodon。",
"description_photo": "為視障人士提供圖片說明...",
- "description_video": "為視障人士提供影片說明..."
+ "description_video": "為視障人士提供影片說明...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
+ "attachment_too_large": "Attachment too large"
},
"poll": {
"duration_time": "持續時間:%s",
@@ -696,7 +706,7 @@
"accessibility_hint": "點兩下以關閉此設定精靈"
},
"bookmark": {
- "title": "Bookmarks"
+ "title": "書籤"
}
}
}
diff --git a/Localization/app.json b/Localization/app.json
index ff2926996..f90b2bef6 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -138,7 +138,7 @@
},
"meta_entity": {
"url": "Link: %s",
- "hashtag": "Hastag %s",
+ "hashtag": "Hashtag: %s",
"mention": "Show Profile: %s",
"email": "Email address: %s"
},
@@ -382,7 +382,13 @@
"video": "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..."
+ "description_video": "Describe the video for the visually-impaired...",
+ "load_failed": "Load Failed",
+ "upload_failed": "Upload Failed",
+ "can_not_recognize_this_media_attachment": "Can not recognize this media attachment",
+ "attachment_too_large": "Attachment too large",
+ "compressing_state": "Compressing...",
+ "server_processing_state": "Server Processing..."
},
"poll": {
"duration_time": "Duration: %s",
@@ -392,7 +398,9 @@
"one_day": "1 Day",
"three_days": "3 Days",
"seven_days": "7 Days",
- "option_number": "Option %ld"
+ "option_number": "Option %ld",
+ "the_poll_is_invalid": "The poll is invalid",
+ "the_poll_has_empty_option": "The poll has empty option"
},
"content_warning": {
"placeholder": "Write an accurate warning here..."
@@ -413,7 +421,9 @@
"custom_emoji_picker": "Custom Emoji Picker",
"enable_content_warning": "Enable Content Warning",
"disable_content_warning": "Disable Content Warning",
- "post_visibility_menu": "Post Visibility Menu"
+ "post_visibility_menu": "Post Visibility Menu",
+ "post_options": "Post Options",
+ "posting_as": "Posting as %s"
},
"keyboard": {
"discard_post": "Discard Post",
@@ -438,6 +448,10 @@
"placeholder": {
"label": "Label",
"content": "Content"
+ },
+ "verified": {
+ "short": "Verified on %s",
+ "long": "Ownership of this link was checked on %s"
}
},
"segmented_control": {
@@ -705,4 +719,4 @@
"title": "Bookmarks"
}
}
-}
+}
\ No newline at end of file
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 380f21eac..434926d94 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -151,7 +151,6 @@
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; };
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
- DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
DB22C92228E700A10082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92128E700A10082A9E9 /* MastodonSDK */; };
DB22C92428E700A80082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92328E700A80082A9E9 /* MastodonSDK */; };
DB22C92628E700AF0082A9E9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB22C92528E700AF0082A9E9 /* MastodonSDK */; };
@@ -185,9 +184,6 @@
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; };
- DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
- DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; };
- DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; };
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
@@ -257,7 +253,6 @@
DB64BA452851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */; };
DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; };
DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; };
- DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; };
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; };
DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */; };
DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */; };
@@ -337,7 +332,6 @@
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */; };
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
- DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
@@ -376,12 +370,11 @@
DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; };
DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; };
- DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; };
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; };
- DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; };
+ DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC3872329214121001EC0FD /* ShareViewController.swift */; };
DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; };
DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.swift */; };
+ DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; };
DBC6462826A1736300B0E31B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; };
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */; };
@@ -681,7 +674,6 @@
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = ""; };
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = ""; };
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; };
- DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = ""; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; };
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = ""; };
@@ -718,9 +710,6 @@
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = ""; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; };
- DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = ""; };
- DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = ""; };
- DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = ""; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = ""; };
@@ -820,7 +809,6 @@
DB64BA442851F23000ADF1B7 /* MastodonAuthentication+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonAuthentication+Fetch.swift"; sourceTree = ""; };
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; };
DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = ""; };
- DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = ""; };
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = ""; };
DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolRelayDelegate.swift; sourceTree = ""; };
DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolDelegate.swift; sourceTree = ""; };
@@ -912,7 +900,6 @@
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = ""; };
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = ""; };
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = ""; };
- DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = ""; };
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; };
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; };
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; };
@@ -961,14 +948,12 @@
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeProfileViewModel.swift; sourceTree = ""; };
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; };
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; };
- DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; };
- DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = ""; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; };
+ DBC3872329214121001EC0FD /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; };
DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
- DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; };
DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; };
DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; };
+ DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; };
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; };
DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = ""; };
DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; };
@@ -1042,15 +1027,7 @@
DBFEEC98279BDCDE004F81DD /* ProfileAboutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAboutViewModel.swift; sourceTree = ""; };
DBFEEC9A279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileAboutViewModel+Diffable.swift"; sourceTree = ""; };
DBFEEC9C279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldEditCollectionViewCell.swift; sourceTree = ""; };
- DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditorView.swift; sourceTree = ""; };
- DBFEF05626A576EE006D7ED1 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; };
- DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; };
- DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningEditorView.swift; sourceTree = ""; };
- DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAuthorView.swift; sourceTree = ""; };
- DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentView.swift; sourceTree = ""; };
- DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentViewModel.swift; sourceTree = ""; };
DBFEF06726A58D07006D7ED1 /* ShareActionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareActionExtension.entitlements; sourceTree = ""; };
- DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusAttachmentViewModel+UploadState.swift"; sourceTree = ""; };
DDB1B139FA8EA26F510D58B6 /* Pods-AppShared.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.asdk - release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.asdk - release.xcconfig"; sourceTree = ""; };
DF65937EC1FF64462BC002EE /* Pods-MastodonTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.profile.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.profile.xcconfig"; sourceTree = ""; };
E5C7236E58D14A0322FE00F2 /* Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - debug.xcconfig"; sourceTree = ""; };
@@ -1342,7 +1319,7 @@
path = Protocol;
sourceTree = "";
};
- 2D76319C25C151DE00929FB9 /* Diffiable */ = {
+ 2D76319C25C151DE00929FB9 /* Diffable */ = {
isa = PBXGroup;
children = (
DB4F097826A039B400D62E92 /* Onboarding */,
@@ -1357,7 +1334,7 @@
DB3E6FE52806A5BA00B035AE /* Discovery */,
DB0617FA27855B660030EE79 /* Settings */,
);
- path = Diffiable;
+ path = Diffable;
sourceTree = "";
};
2D7631A425C1532200929FB9 /* Share */ = {
@@ -1762,7 +1739,7 @@
children = (
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
DB427DE325BAA00100D1B89D /* Info.plist */,
- 2D76319C25C151DE00929FB9 /* Diffiable */,
+ 2D76319C25C151DE00929FB9 /* Diffable */,
DB8AF55525C1379F002E6C99 /* Scene */,
DB8AF54125C13647002E6C99 /* Coordinator */,
DB8AF56225C138BC002E6C99 /* Extension */,
@@ -1878,8 +1855,6 @@
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
- DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */,
- DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */,
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */,
);
path = View;
@@ -2146,8 +2121,6 @@
DB789A2125F9F76D0071ACA0 /* CollectionViewCell */,
DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */,
DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */,
- DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */,
- DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */,
);
path = Compose;
sourceTree = "";
@@ -2159,8 +2132,6 @@
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
- DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */,
- DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */,
);
path = CollectionViewCell;
sourceTree = "";
@@ -2520,7 +2491,6 @@
DBBC24D526A54BCB00398BB9 /* Helper */ = {
isa = PBXGroup;
children = (
- DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */,
DBF3B7402733EB9400E21627 /* MastodonLocalCode.swift */,
);
path = Helper;
@@ -2687,28 +2657,11 @@
path = Cell;
sourceTree = "";
};
- DBFEF05426A576EE006D7ED1 /* View */ = {
- isa = PBXGroup;
- children = (
- DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */,
- DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */,
- DBFEF05626A576EE006D7ED1 /* ComposeView.swift */,
- DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */,
- DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */,
- DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */,
- DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */,
- DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */,
- DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */,
- );
- path = View;
- sourceTree = "";
- };
DBFEF06126A57721006D7ED1 /* Scene */ = {
isa = PBXGroup;
children = (
- DBFEF05426A576EE006D7ED1 /* View */,
- DBC6462226A1712000B0E31B /* ComposeViewModel.swift */,
- DBC6461426A170AB00B0E31B /* ComposeViewController.swift */,
+ DBC6462226A1712000B0E31B /* ShareViewModel.swift */,
+ DBC3872329214121001EC0FD /* ShareViewController.swift */,
);
path = Scene;
sourceTree = "";
@@ -3198,7 +3151,6 @@
0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
- DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
@@ -3246,7 +3198,6 @@
DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */,
DB3E6FEC2806D7F100B035AE /* DiscoveryNewsViewController.swift in Sources */,
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
- DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */,
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */,
@@ -3301,7 +3252,6 @@
DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */,
2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */,
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
- DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */,
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
@@ -3339,7 +3289,6 @@
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */,
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
- DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */,
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
@@ -3353,7 +3302,6 @@
DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */,
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */,
- DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */,
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
@@ -3437,7 +3385,6 @@
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
- DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
@@ -3461,7 +3408,6 @@
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
- DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */,
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
@@ -3567,9 +3513,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */,
+ DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */,
DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */,
- DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */,
+ DBC3872429214121001EC0FD /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 979c8c0e6..78a3a9e70 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -117,7 +117,7 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 18
+ 16
ShareActionExtension.xcscheme_^#shared#^_
diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 64dc691bb..409b8820d 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -90,6 +90,15 @@
"version" : "2.2.5"
}
},
+ {
+ "identity" : "nextlevelsessionexporter",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/NextLevel/NextLevelSessionExporter.git",
+ "state" : {
+ "revision" : "b6c0cce1aa37fe1547d694f958fac3c3524b74da",
+ "version" : "0.4.6"
+ }
+ },
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
diff --git a/Mastodon/Diffiable/Account/SelectedAccountItem.swift b/Mastodon/Diffable/Account/SelectedAccountItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Account/SelectedAccountItem.swift
rename to Mastodon/Diffable/Account/SelectedAccountItem.swift
diff --git a/Mastodon/Diffiable/Account/SelectedAccountSection.swift b/Mastodon/Diffable/Account/SelectedAccountSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Account/SelectedAccountSection.swift
rename to Mastodon/Diffable/Account/SelectedAccountSection.swift
diff --git a/Mastodon/Diffiable/Discovery/DiscoveryItem.swift b/Mastodon/Diffable/Discovery/DiscoveryItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Discovery/DiscoveryItem.swift
rename to Mastodon/Diffable/Discovery/DiscoveryItem.swift
diff --git a/Mastodon/Diffiable/Discovery/DiscoverySection.swift b/Mastodon/Diffable/Discovery/DiscoverySection.swift
similarity index 100%
rename from Mastodon/Diffiable/Discovery/DiscoverySection.swift
rename to Mastodon/Diffable/Discovery/DiscoverySection.swift
diff --git a/Mastodon/Diffiable/Notification/NotificationItem.swift b/Mastodon/Diffable/Notification/NotificationItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Notification/NotificationItem.swift
rename to Mastodon/Diffable/Notification/NotificationItem.swift
diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffable/Notification/NotificationSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Notification/NotificationSection.swift
rename to Mastodon/Diffable/Notification/NotificationSection.swift
diff --git a/Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift b/Mastodon/Diffable/Onboarding/CategoryPickerItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/CategoryPickerItem.swift
rename to Mastodon/Diffable/Onboarding/CategoryPickerItem.swift
diff --git a/Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift b/Mastodon/Diffable/Onboarding/CategoryPickerSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/CategoryPickerSection.swift
rename to Mastodon/Diffable/Onboarding/CategoryPickerSection.swift
diff --git a/Mastodon/Diffiable/Onboarding/PickServerItem.swift b/Mastodon/Diffable/Onboarding/PickServerItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/PickServerItem.swift
rename to Mastodon/Diffable/Onboarding/PickServerItem.swift
diff --git a/Mastodon/Diffiable/Onboarding/PickServerSection.swift b/Mastodon/Diffable/Onboarding/PickServerSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/PickServerSection.swift
rename to Mastodon/Diffable/Onboarding/PickServerSection.swift
diff --git a/Mastodon/Diffiable/Onboarding/RegisterItem.swift b/Mastodon/Diffable/Onboarding/RegisterItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/RegisterItem.swift
rename to Mastodon/Diffable/Onboarding/RegisterItem.swift
diff --git a/Mastodon/Diffiable/Onboarding/RegisterSection.swift b/Mastodon/Diffable/Onboarding/RegisterSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/RegisterSection.swift
rename to Mastodon/Diffable/Onboarding/RegisterSection.swift
diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleItem.swift b/Mastodon/Diffable/Onboarding/ServerRuleItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/ServerRuleItem.swift
rename to Mastodon/Diffable/Onboarding/ServerRuleItem.swift
diff --git a/Mastodon/Diffiable/Onboarding/ServerRuleSection.swift b/Mastodon/Diffable/Onboarding/ServerRuleSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Onboarding/ServerRuleSection.swift
rename to Mastodon/Diffable/Onboarding/ServerRuleSection.swift
diff --git a/Mastodon/Diffiable/Profile/ProfileFieldItem.swift b/Mastodon/Diffable/Profile/ProfileFieldItem.swift
similarity index 86%
rename from Mastodon/Diffiable/Profile/ProfileFieldItem.swift
rename to Mastodon/Diffable/Profile/ProfileFieldItem.swift
index 47848cc01..e33a2f883 100644
--- a/Mastodon/Diffiable/Profile/ProfileFieldItem.swift
+++ b/Mastodon/Diffable/Profile/ProfileFieldItem.swift
@@ -23,6 +23,7 @@ extension ProfileFieldItem {
var name: CurrentValueSubject
var value: CurrentValueSubject
+ var verifiedAt: CurrentValueSubject
let emojiMeta: MastodonContent.Emojis
@@ -30,11 +31,13 @@ extension ProfileFieldItem {
id: UUID = UUID(),
name: String,
value: String,
+ verifiedAt: Date?,
emojiMeta: MastodonContent.Emojis
) {
self.id = id
self.name = CurrentValueSubject(name)
self.value = CurrentValueSubject(value)
+ self.verifiedAt = CurrentValueSubject(verifiedAt)
self.emojiMeta = emojiMeta
}
@@ -45,6 +48,7 @@ extension ProfileFieldItem {
return lhs.id == rhs.id
&& lhs.name.value == rhs.name.value
&& lhs.value.value == rhs.value.value
+ && lhs.verifiedAt.value == rhs.verifiedAt.value
&& lhs.emojiMeta == rhs.emojiMeta
}
diff --git a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift b/Mastodon/Diffable/Profile/ProfileFieldSection.swift
similarity index 87%
rename from Mastodon/Diffiable/Profile/ProfileFieldSection.swift
rename to Mastodon/Diffable/Profile/ProfileFieldSection.swift
index 19771b5db..6e57e6af9 100644
--- a/Mastodon/Diffiable/Profile/ProfileFieldSection.swift
+++ b/Mastodon/Diffable/Profile/ProfileFieldSection.swift
@@ -8,6 +8,7 @@
import os
import UIKit
import Combine
+import MastodonAsset
import MastodonCore
import MastodonMeta
import MastodonLocalization
@@ -48,6 +49,10 @@ extension ProfileFieldSection {
do {
let mastodonContent = MastodonContent(content: field.value.value, emojis: field.emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
+ cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Colors.brand.color
+ if field.verifiedAt.value != nil {
+ cell.valueMetaLabel.linkAttributes[.foregroundColor] = Asset.Scene.Profile.About.bioAboutFieldVerifiedLink.color
+ }
cell.valueMetaLabel.configure(content: metaContent)
} catch {
let content = PlaintextMetaContent(string: field.value.value)
@@ -57,7 +62,23 @@ extension ProfileFieldSection {
// set background
var backgroundConfiguration = UIBackgroundConfiguration.listPlainCell()
backgroundConfiguration.backgroundColor = UIColor.secondarySystemBackground
+ if (field.verifiedAt.value != nil) {
+ backgroundConfiguration.backgroundColor = Asset.Scene.Profile.About.bioAboutFieldVerifiedBackground.color
+ }
cell.backgroundConfiguration = backgroundConfiguration
+
+ // set checkmark and edit menu label
+ cell.checkmark.isHidden = true
+ cell.checkmarkPopoverString = nil
+ if let verifiedAt = field.verifiedAt.value {
+ cell.checkmark.isHidden = false
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .short
+ let dateString = formatter.string(from: verifiedAt)
+ cell.checkmark.accessibilityLabel = L10n.Scene.Profile.Fields.Verified.long(dateString)
+ cell.checkmarkPopoverString = L10n.Scene.Profile.Fields.Verified.short(dateString)
+ }
cell.delegate = configuration.profileFieldCollectionViewCellDelegate
}
diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountItem.swift b/Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift
similarity index 100%
rename from Mastodon/Diffiable/RecommandAccount/RecommendAccountItem.swift
rename to Mastodon/Diffable/RecommandAccount/RecommendAccountItem.swift
diff --git a/Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift b/Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift
similarity index 100%
rename from Mastodon/Diffiable/RecommandAccount/RecommendAccountSection.swift
rename to Mastodon/Diffable/RecommandAccount/RecommendAccountSection.swift
diff --git a/Mastodon/Diffiable/Report/ReportItem.swift b/Mastodon/Diffable/Report/ReportItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Report/ReportItem.swift
rename to Mastodon/Diffable/Report/ReportItem.swift
diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffable/Report/ReportSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Report/ReportSection.swift
rename to Mastodon/Diffable/Report/ReportSection.swift
diff --git a/Mastodon/Diffiable/Search/SearchHistoryItem.swift b/Mastodon/Diffable/Search/SearchHistoryItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchHistoryItem.swift
rename to Mastodon/Diffable/Search/SearchHistoryItem.swift
diff --git a/Mastodon/Diffiable/Search/SearchHistorySection.swift b/Mastodon/Diffable/Search/SearchHistorySection.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchHistorySection.swift
rename to Mastodon/Diffable/Search/SearchHistorySection.swift
diff --git a/Mastodon/Diffiable/Search/SearchItem.swift b/Mastodon/Diffable/Search/SearchItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchItem.swift
rename to Mastodon/Diffable/Search/SearchItem.swift
diff --git a/Mastodon/Diffiable/Search/SearchResultItem.swift b/Mastodon/Diffable/Search/SearchResultItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchResultItem.swift
rename to Mastodon/Diffable/Search/SearchResultItem.swift
diff --git a/Mastodon/Diffiable/Search/SearchResultSection.swift b/Mastodon/Diffable/Search/SearchResultSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchResultSection.swift
rename to Mastodon/Diffable/Search/SearchResultSection.swift
diff --git a/Mastodon/Diffiable/Search/SearchSection.swift b/Mastodon/Diffable/Search/SearchSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Search/SearchSection.swift
rename to Mastodon/Diffable/Search/SearchSection.swift
diff --git a/Mastodon/Diffiable/Settings/SettingsItem.swift b/Mastodon/Diffable/Settings/SettingsItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Settings/SettingsItem.swift
rename to Mastodon/Diffable/Settings/SettingsItem.swift
diff --git a/Mastodon/Diffiable/Settings/SettingsSection.swift b/Mastodon/Diffable/Settings/SettingsSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Settings/SettingsSection.swift
rename to Mastodon/Diffable/Settings/SettingsSection.swift
diff --git a/Mastodon/Diffiable/Status/StatusItem.swift b/Mastodon/Diffable/Status/StatusItem.swift
similarity index 100%
rename from Mastodon/Diffiable/Status/StatusItem.swift
rename to Mastodon/Diffable/Status/StatusItem.swift
diff --git a/Mastodon/Diffiable/Status/StatusSection.swift b/Mastodon/Diffable/Status/StatusSection.swift
similarity index 100%
rename from Mastodon/Diffiable/Status/StatusSection.swift
rename to Mastodon/Diffable/Status/StatusSection.swift
diff --git a/Mastodon/Diffiable/User/UserItem.swift b/Mastodon/Diffable/User/UserItem.swift
similarity index 100%
rename from Mastodon/Diffiable/User/UserItem.swift
rename to Mastodon/Diffable/User/UserItem.swift
diff --git a/Mastodon/Diffiable/User/UserSection.swift b/Mastodon/Diffable/User/UserSection.swift
similarity index 100%
rename from Mastodon/Diffiable/User/UserSection.swift
rename to Mastodon/Diffable/User/UserSection.swift
diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift
index bf9145d6c..fd5ed5817 100644
--- a/Mastodon/Scene/Compose/ComposeViewController.swift
+++ b/Mastodon/Scene/Compose/ComposeViewController.swift
@@ -44,18 +44,6 @@ final class ComposeViewController: UIViewController, NeedsDependency {
}()
private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
- let characterCountLabel: UILabel = {
- let label = UILabel()
- label.font = .systemFont(ofSize: 15, weight: .regular)
- label.text = "500"
- label.textColor = Asset.Colors.Label.secondary.color
- label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
- return label
- }()
- private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
- let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
- return barButtonItem
- }()
let publishButton: UIButton = {
let button = RoundedEdgesButton(type: .custom)
@@ -86,54 +74,13 @@ final class ComposeViewController: UIViewController, NeedsDependency {
publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
}
-
-// var systemKeyboardHeight: CGFloat = .zero {
-// didSet {
-// // note: some system AutoLayout warning here
-// let height = max(300, systemKeyboardHeight)
-// customEmojiPickerInputView.frame.size.height = height
-// }
-// }
-//
-// // CustomEmojiPickerView
-// let customEmojiPickerInputView: CustomEmojiPickerInputView = {
-// let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard)
-// return view
-// }()
-//
-// let composeToolbarView = ComposeToolbarView()
-// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
-// let composeToolbarBackgroundView = UIView()
-//
-//
-// private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
-// let viewController = AutoCompleteViewController()
-// viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext)
-// viewController.delegate = self
-// viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
-// return viewController
-// }()
-
+
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
-extension ComposeViewController {
- private static func createLayout() -> UICollectionViewLayout {
- let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
- let item = NSCollectionLayoutItem(layoutSize: itemSize)
- let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
- let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
- let section = NSCollectionLayoutSection(group: group)
- section.contentInsetsReference = .readableContent
- // section.interGroupSpacing = 10
- // section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
- return UICollectionViewCompositionalLayout(section: section)
- }
-}
-
extension ComposeViewController {
override func viewDidLoad() {
@@ -147,9 +94,9 @@ extension ComposeViewController {
guard let self = self else { return }
guard self.traitCollection.userInterfaceIdiom == .pad else { return }
var items = [self.publishBarButtonItem]
- if self.traitCollection.horizontalSizeClass == .regular {
- items.append(self.characterCountBarButtonItem)
- }
+ // if self.traitCollection.horizontalSizeClass == .regular {
+ // items.append(self.characterCountBarButtonItem)
+ // }
self.navigationItem.rightBarButtonItems = items
}
.store(in: &disposeBag)
@@ -166,458 +113,71 @@ extension ComposeViewController {
])
composeContentViewController.didMove(toParent: self)
-// configureNavigationBarTitleStyle()
-// viewModel.traitCollectionDidChangePublisher
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] _ in
-// guard let self = self else { return }
-// self.configureNavigationBarTitleStyle()
-// }
-// .store(in: &disposeBag)
-//
-// viewModel.$title
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] title in
-// guard let self = self else { return }
-// self.title = title
-// }
-// .store(in: &disposeBag)
-//
-// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
-// view.addSubview(composeToolbarView)
-// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
-// NSLayoutConstraint.activate([
-// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
-// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
-// composeToolbarViewBottomLayoutConstraint,
-// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
-// ])
-// composeToolbarView.preservesSuperviewLayoutMargins = true
-// composeToolbarView.delegate = self
-//
-// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
-// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
-// NSLayoutConstraint.activate([
-// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
-// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
-// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
-// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
-// ])
+ // bind title
+ viewModel.$title
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] title in
+ guard let self = self else { return }
+ self.title = title
+ }
+ .store(in: &disposeBag)
-// tableView.delegate = self
-// viewModel.setupDataSource(
-// tableView: tableView,
-// metaTextDelegate: self,
-// metaTextViewDelegate: self,
-// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
-// composeStatusAttachmentCollectionViewCellDelegate: self,
-// composeStatusPollOptionCollectionViewCellDelegate: self,
-// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
-// composeStatusPollExpiresOptionCollectionViewCellDelegate: self
-// )
-
-// viewModel.composeStatusAttribute.$composeContent
-// .removeDuplicates()
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] _ in
-// guard let self = self else { return }
-// guard self.view.window != nil else { return }
-// UIView.performWithoutAnimation {
-// self.tableView.beginUpdates()
-// self.tableView.setNeedsLayout()
-// self.tableView.layoutIfNeeded()
-// self.tableView.endUpdates()
-// }
-// }
-// .store(in: &disposeBag)
-
-// customEmojiPickerInputView.collectionView.delegate = self
-// viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
-// viewModel.setupCustomEmojiPickerDiffableDataSource(
-// for: customEmojiPickerInputView.collectionView,
-// dependency: self
-// )
-
-// viewModel.composeStatusContentTableViewCell.delegate = self
-//
-// // update layout when keyboard show/dismiss
-// view.layoutIfNeeded()
-//
-
-//
-// // bind auto-complete
-// viewModel.$autoCompleteInfo
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] info in
-// guard let self = self else { return }
-// let textEditorView = self.textEditorView
-// if self.autoCompleteViewController.view.superview == nil {
-// self.autoCompleteViewController.view.frame = self.view.bounds
-// // add to container view. seealso: `viewDidLayoutSubviews()`
-// self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view)
-// self.addChild(self.autoCompleteViewController)
-// self.autoCompleteViewController.didMove(toParent: self)
-// self.autoCompleteViewController.view.isHidden = true
-// self.tableView.autoCompleteViewController = self.autoCompleteViewController
-// }
-// self.updateAutoCompleteViewControllerLayout()
-// self.autoCompleteViewController.view.isHidden = info == nil
-// guard let info = info else { return }
-// let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
-// self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
-// self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
-// self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
-// }
-// .store(in: &disposeBag)
-//
-// // bind publish bar button state
-// viewModel.$isPublishBarButtonItemEnabled
-// .receive(on: DispatchQueue.main)
-// .assign(to: \.isEnabled, on: publishButton)
-// .store(in: &disposeBag)
-//
-// // bind media button toolbar state
-// viewModel.$isMediaToolbarButtonEnabled
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isMediaToolbarButtonEnabled in
-// guard let self = self else { return }
-// self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled
-// self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled
-// }
-// .store(in: &disposeBag)
-//
-// // bind poll button toolbar state
-// viewModel.$isPollToolbarButtonEnabled
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isPollToolbarButtonEnabled in
-// guard let self = self else { return }
-// self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled
-// self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled
-// }
-// .store(in: &disposeBag)
-//
-// Publishers.CombineLatest(
-// viewModel.$isPollComposing,
-// viewModel.$isPollToolbarButtonEnabled
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
-// guard let self = self else { return }
-// guard isPollToolbarButtonEnabled else {
-// let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
-// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
-// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
-// return
-// }
-// let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
-// self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
-// self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
-// }
-// .store(in: &disposeBag)
-//
-// // bind image picker toolbar state
-// viewModel.$attachmentServices
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] attachmentServices in
-// guard let self = self else { return }
-// let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments
-// self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled
-// self.composeToolbarView.mediaButton.isEnabled = isEnabled
-// self.resetImagePicker()
-// }
-// .store(in: &disposeBag)
-//
-// // bind content warning button state
-// viewModel.$isContentWarningComposing
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isContentWarningComposing in
-// guard let self = self else { return }
-// let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
-// self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel
-// self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel
-// }
-// .store(in: &disposeBag)
-//
-// // bind visibility toolbar UI
-// Publishers.CombineLatest(
-// viewModel.$selectedStatusVisibility,
-// viewModel.traitCollectionDidChangePublisher
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] type, _ in
-// guard let self = self else { return }
-// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
-// self.composeToolbarView.visibilityBarButtonItem.image = image
-// self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
-// self.composeToolbarView.activeVisibilityType.value = type
-// }
-// .store(in: &disposeBag)
-//
-// viewModel.$characterCount
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] characterCount in
-// guard let self = self else { return }
-// let count = self.viewModel.composeContentLimit - characterCount
-// self.composeToolbarView.characterCountLabel.text = "\(count)"
-// self.characterCountLabel.text = "\(count)"
-// let font: UIFont
-// let textColor: UIColor
-// let accessibilityLabel: String
-// switch count {
-// case _ where count < 0:
-// font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
-// textColor = Asset.Colors.danger.color
-// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count))
-// default:
-// font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
-// textColor = Asset.Colors.Label.secondary.color
-// accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count)
-// }
-// self.composeToolbarView.characterCountLabel.font = font
-// self.composeToolbarView.characterCountLabel.textColor = textColor
-// self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel
-// self.characterCountLabel.font = font
-// self.characterCountLabel.textColor = textColor
-// self.characterCountLabel.accessibilityLabel = accessibilityLabel
-// self.characterCountLabel.sizeToFit()
-// }
-// .store(in: &disposeBag)
-//
-// // bind custom emoji picker UI
-// viewModel.customEmojiViewModel?.emojis
-// .receive(on: DispatchQueue.main)
-// .sink(receiveValue: { [weak self] emojis in
-// guard let self = self else { return }
-// if emojis.isEmpty {
-// self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
-// } else {
-// self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
-// }
-// })
-// .store(in: &disposeBag)
-//
-// configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
-// Publishers.CombineLatest(
-// keyboardHasShortcutBar,
-// viewModel.traitCollectionDidChangePublisher
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] keyboardHasShortcutBar, _ in
-// guard let self = self else { return }
-// self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar)
-// }
-// .store(in: &disposeBag)
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
-// // update MetaText without trigger call underlaying `UITextStorage.processEditing`
-// _ = textEditorView.processEditing(textEditorView.textStorage)
-
-// markTextEditorViewBecomeFirstResponser()
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
-// viewModel.isViewAppeared = true
+ // bind publish bar button state
+ composeContentViewModel.$isPublishBarButtonItemEnabled
+ .receive(on: DispatchQueue.main)
+ .assign(to: \.isEnabled, on: publishButton)
+ .store(in: &disposeBag)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
-// configurePublishButtonApperance()
-// viewModel.traitCollectionDidChangePublisher.send()
- }
-
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
-
- updateAutoCompleteViewControllerLayout()
- }
-
- private func updateAutoCompleteViewControllerLayout() {
- // pin autoCompleteViewController frame to current view
-// if let containerView = autoCompleteViewController.view.superview {
-// let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
-// if viewFrameInWindow.origin.x != 0 {
-// autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
-// }
-// autoCompleteViewController.view.frame.size.width = view.frame.width
-// }
+ configurePublishButtonApperance()
+ viewModel.traitCollectionDidChangePublisher.send()
}
}
-//extension ComposeViewController {
-//
-// private var textEditorView: MetaText {
-// return viewModel.composeStatusContentTableViewCell.metaText
-// }
-//
-// private func markTextEditorViewBecomeFirstResponser() {
-// textEditorView.textView.becomeFirstResponder()
-// }
-//
-// private func contentWarningEditorTextView() -> UITextView? {
-// viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView
-// }
-//
-// private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? {
-// guard case .pollOption = item else { return nil }
-// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
-// guard let indexPath = dataSource.indexPath(for: item),
-// let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
-// return nil
-// }
-//
-// return cell
-// }
-//
-// private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
-// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
-// let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
-// let firstPollItem = items.first { item -> Bool in
-// guard case .pollOption = item else { return false }
-// return true
-// }
-//
-// guard let item = firstPollItem else {
-// return nil
-// }
-//
-// return pollOptionCollectionViewCell(of: item)
-// }
-//
-// private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
-// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
-// let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
-// let lastPollItem = items.last { item -> Bool in
-// guard case .pollOption = item else { return false }
-// return true
-// }
-//
-// guard let item = lastPollItem else {
-// return nil
-// }
-//
-// return pollOptionCollectionViewCell(of: item)
-// }
-//
-// private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
-// guard let cell = firstPollOptionCollectionViewCell() else { return }
-// cell.pollOptionView.optionTextField.becomeFirstResponder()
-// }
-//
-// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
-// guard let cell = lastPollOptionCollectionViewCell() else { return }
-// cell.pollOptionView.optionTextField.becomeFirstResponder()
-// }
-//
-// private func showDismissConfirmAlertController() {
-// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
-// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
-// guard let self = self else { return }
-// self.dismiss(animated: true, completion: nil)
-// }
-// alertController.addAction(discardAction)
-// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
-// alertController.addAction(cancelAction)
-// alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem
-// present(alertController, animated: true, completion: nil)
-// }
-//
-// private func resetImagePicker() {
-// let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count)
-// let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
-// photoLibraryPicker = createImagePicker(configuration: configuration)
-// }
-//
-// private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
-// let imagePicker = PHPickerViewController(configuration: configuration)
-// imagePicker.delegate = self
-// return imagePicker
-// }
-//
-// private func setupBackgroundColor(theme: Theme) {
-// let backgroundColor = UIColor(dynamicProvider: { traitCollection in
-// switch traitCollection.userInterfaceStyle {
-// case .light:
-// return .systemBackground
-// default:
-// return theme.systemElevatedBackgroundColor
-// }
-// })
-// view.backgroundColor = backgroundColor
-//// tableView.backgroundColor = backgroundColor
-//// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor
-// }
-//
-// // keyboard shortcutBar
-// private func setupInputAssistantItem(item: UITextInputAssistantItem) {
-// let barButtonItems = [
-// composeToolbarView.mediaBarButtonItem,
-// composeToolbarView.pollBarButtonItem,
-// composeToolbarView.contentWarningBarButtonItem,
-// composeToolbarView.visibilityBarButtonItem,
-// ]
-// let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil)
-//
-// item.trailingBarButtonGroups = [group]
-// }
-//
-// private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) {
-// switch self.traitCollection.userInterfaceIdiom {
-// case .pad:
-// let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular
-// self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1
-// self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1
-// default:
-// break
-// }
-// }
-//
-// private func configureNavigationBarTitleStyle() {
-// switch traitCollection.userInterfaceIdiom {
-// case .pad:
-// navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular
-// default:
-// break
-// }
-// }
-//
-//}
-//
+extension ComposeViewController {
+
+ private func showDismissConfirmAlertController() {
+ let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
+ let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
+ guard let self = self else { return }
+ self.dismiss(animated: true, completion: nil)
+ }
+ alertController.addAction(discardAction)
+ let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
+ alertController.addAction(cancelAction)
+ alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem
+ present(alertController, animated: true, completion: nil)
+ }
+
+}
+
extension ComposeViewController {
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-// guard viewModel.shouldDismiss else {
-// showDismissConfirmAlertController()
-// return
-// }
+ guard composeContentViewModel.shouldDismiss else {
+ showDismissConfirmAlertController()
+ return
+ }
dismiss(animated: true, completion: nil)
}
@objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-// do {
-// try viewModel.checkAttachmentPrecondition()
-// } catch {
-// let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
-// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
-// alertController.addAction(okAction)
-// coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
-// return
-// }
-// guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else {
-// // TODO: handle error
-// return
-// }
-
- // context.statusPublishService.publish(composeViewModel: viewModel)
+ do {
+ try composeContentViewModel.checkAttachmentPrecondition()
+ } catch {
+ let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
+ let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
+ alertController.addAction(okAction)
+ coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
+ return
+ }
do {
let statusPublisher = try composeContentViewModel.statusPublisher()
@@ -640,249 +200,35 @@ extension ComposeViewController {
}
-//// MARK: - MetaTextDelegate
-//extension ComposeViewController: MetaTextDelegate {
-// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
-// let string = metaText.textStorage.string
-// let content = MastodonContent(
-// content: string,
-// emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:]
-// )
-// let metaContent = MastodonMetaContent.convert(text: content)
-// return metaContent
-// }
-//}
-//
-//// MARK: - UITextViewDelegate
-//extension ComposeViewController: UITextViewDelegate {
-//
-// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
-// setupInputAssistantItem(item: textView.inputAssistantItem)
-// return true
-// }
-//
-// func textViewDidChange(_ textView: UITextView) {
-// switch textView {
-// case textEditorView.textView:
-// // update model
-// let metaText = self.textEditorView
-// let backedString = metaText.backedString
-// viewModel.composeStatusAttribute.composeContent = backedString
-// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
-//
-// // configure auto completion
-// setupAutoComplete(for: textView)
-// default:
-// assertionFailure()
-// }
-// }
-//
-// struct AutoCompleteInfo {
-// // model
-// let inputText: Substring
-// // range
-// let symbolRange: Range
-// let symbolString: Substring
-// let toCursorRange: Range
-// let toCursorString: Substring
-// let toHighlightEndRange: Range
-// let toHighlightEndString: Substring
-// // geometry
-// var textBoundingRect: CGRect = .zero
-// var symbolBoundingRect: CGRect = .zero
-// }
-//
-// private func setupAutoComplete(for textView: UITextView) {
-// guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else {
-// viewModel.autoCompleteInfo = nil
-// return
-// }
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
-//
-// // get layout text bounding rect
-// var glyphRange = NSRange()
-// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
-// let textContainer = textView.layoutManager.textContainers[0]
-// let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
-//
-// let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes
-// guard textBoundingRect.size != .zero else {
-// viewModel.autoCompleteRetryLayoutTimes += 1
-// // avoid infinite loop
-// guard retryLayoutTimes < 3 else { return }
-// // needs retry calculate layout when the rect position changing
-// DispatchQueue.main.async {
-// self.setupAutoComplete(for: textView)
-// }
-// return
-// }
-// viewModel.autoCompleteRetryLayoutTimes = 0
-//
-// // get symbol bounding rect
-// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
-// let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
-//
-// // set bounding rect and trigger layout
-// autoCompletion.textBoundingRect = textBoundingRect
-// autoCompletion.symbolBoundingRect = symbolBoundingRect
-// viewModel.autoCompleteInfo = autoCompletion
-// }
-//
-// private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
-// guard let text = textView.text,
-// textView.selectedRange.location > 0, !text.isEmpty,
-// let selectedRange = Range(textView.selectedRange, in: text) else {
-// return nil
-// }
-// let cursorIndex = selectedRange.upperBound
-// let _highlightStartIndex: String.Index? = {
-// var index = text.index(before: cursorIndex)
-// while index > text.startIndex {
-// let char = text[index]
-// if char == "@" || char == "#" || char == ":" {
-// return index
-// }
-// index = text.index(before: index)
-// }
-// assert(index == text.startIndex)
-// let char = text[index]
-// if char == "@" || char == "#" || char == ":" {
-// return index
-// } else {
-// return nil
-// }
-// }()
-//
-// guard let highlightStartIndex = _highlightStartIndex else { return nil }
-// let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil }
-// let symbolRange = highlightStartIndex.. Bool {
-// switch textView {
-// case textEditorView.textView:
-// return false
-// default:
-// return true
-// }
-// }
-//
-// func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
-// switch textView {
-// case textEditorView.textView:
-// return false
-// default:
-// return true
-// }
-// }
-//
-//}
-//
-//// MARK: - ComposeToolbarViewDelegate
-//extension ComposeViewController: ComposeToolbarViewDelegate {
+extension ComposeViewController {
+ public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+
+ // Enable pasting images
+ if (action == #selector(UIResponderStandardEditActions.paste(_:))) {
+ return UIPasteboard.general.hasStrings || UIPasteboard.general.hasImages;
+ }
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) {
-// // toggle poll composing state
-// viewModel.isPollComposing.toggle()
-//
-// // cancel custom picker input
-// viewModel.isCustomEmojiComposing = false
-//
-// // setup initial poll option if needs
-// if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty {
-// viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()]
-// }
-//
-// if viewModel.isPollComposing {
-// // Magic RunLoop
-// DispatchQueue.main.async {
-// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
-// }
-// } else {
-// markTextEditorViewBecomeFirstResponser()
-// }
-// }
-//
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) {
-// viewModel.isCustomEmojiComposing.toggle()
-// }
-//
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) {
-// // cancel custom picker input
-// viewModel.isCustomEmojiComposing = false
-//
-// // restore first responder for text editor when content warning dismiss
-// if viewModel.isContentWarningComposing {
-// if contentWarningEditorTextView()?.isFirstResponder == true {
-// markTextEditorViewBecomeFirstResponser()
-// }
-// }
-//
-// // toggle composing status
-// viewModel.isContentWarningComposing.toggle()
-//
-// // active content warning after toggled
-// if viewModel.isContentWarningComposing {
-// contentWarningEditorTextView()?.becomeFirstResponder()
-// }
-// }
-//
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
-// viewModel.selectedStatusVisibility = type
-// }
-//
-//}
+ return super.canPerformAction(action, withSender: sender);
+ }
+
+ override func paste(_ sender: Any?) {
+ logger.debug("Paste event received")
-//// MARK: - UITableViewDelegate
-//extension ComposeViewController: UITableViewDelegate { }
-//
-//// MARK: - UICollectionViewDelegate
-//extension ComposeViewController: UICollectionViewDelegate {
-//
-// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
-//
-// if collectionView === customEmojiPickerInputView.collectionView {
-// guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
-// let item = diffableDataSource.itemIdentifier(for: indexPath)
-// guard case let .emoji(attribute) = item else { return }
-// let emoji = attribute.emoji
-//
-// // make click sound
-// UIDevice.current.playInputClick()
-//
-// // retrieve active text input and insert emoji
-// // the trailing space is REQUIRED to make regex happy
-// _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ")
-// } else {
-// // do nothing
-// }
-// }
-//}
+ // Look for images on the clipboard
+ if UIPasteboard.general.hasImages, let images = UIPasteboard.general.images {
+ logger.warning("Got image paste event, however attachments are not yet re-implemented.");
+ let attachmentViewModels = images.map { image in
+ return AttachmentViewModel(
+ api: viewModel.context.apiService,
+ authContext: viewModel.authContext,
+ input: .image(image),
+ delegate: composeContentViewModel
+ )
+ }
+ composeContentViewModel.attachmentViewModels += attachmentViewModels
+ }
+ }
+}
// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
@@ -895,15 +241,15 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
return .pageSheet
}
}
-
-// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
-// return viewModel.shouldDismiss
-// }
-// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-// showDismissConfirmAlertController()
-// }
+ func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
+ return composeContentViewModel.shouldDismiss
+ }
+
+ func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ showDismissConfirmAlertController()
+ }
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@@ -911,304 +257,137 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
}
-//// MARK: - ComposeStatusAttachmentTableViewCellDelegate
-//extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
-//
-// func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
-// guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return }
-// guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return }
-// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
-// guard case let .attachment(attachmentService) = item else { return }
-//
-// var attachmentServices = viewModel.attachmentServices
-// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
-// let removedItem = attachmentServices[index]
-// attachmentServices.remove(at: index)
-// viewModel.attachmentServices = attachmentServices
-//
-// // cancel task
-// removedItem.disposeBag.removeAll()
-// }
-//
-//}
-//
-//// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
-//extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
-//
-// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) {
-//
-// setupInputAssistantItem(item: textField.inputAssistantItem)
-//
-// // FIXME: make poll section visible
-// // DispatchQueue.main.async {
-// // self.collectionView.scroll(to: .bottom, animated: true)
-// // }
-// }
-//
-//
-// // handle delete backward event for poll option input
-// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
-// guard (text ?? "").isEmpty else { return }
-// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
-// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
-// guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
-// guard case let .pollOption(attribute) = item else { return }
-//
-// var pollAttributes = viewModel.pollOptionAttributes
-// guard let index = pollAttributes.firstIndex(of: attribute) else { return }
-//
-// // mark previous (fallback to next) item of removed middle poll option become first responder
-// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main)
-// if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
-// func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
-// guard index > 0 else { return nil }
-// let indexBeforeRemoved = pollItems.index(before: indexOfItem)
-// let itemBeforeRemoved = pollItems[indexBeforeRemoved]
-// return pollOptionCollectionViewCell(of: itemBeforeRemoved)
-// }
-//
-// func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
-// guard index < pollItems.count - 1 else { return nil }
-// let indexAfterRemoved = pollItems.index(after: index)
-// let itemAfterRemoved = pollItems[indexAfterRemoved]
-// return pollOptionCollectionViewCell(of: itemAfterRemoved)
-// }
-//
-// var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
-// if cell == nil {
-// cell = cellAfterRemoved()
-// }
-// cell?.pollOptionView.optionTextField.becomeFirstResponder()
-// }
-//
-// guard pollAttributes.count > 2 else {
-// return
-// }
-// pollAttributes.remove(at: index)
-//
-// // update data source
-// viewModel.pollOptionAttributes = pollAttributes
-// }
-//
-// // handle keyboard return event for poll option input
-// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
-// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
-// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
-// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in
-// guard case .pollOption = item else { return false }
-// return true
-// }
-// guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
-// guard let index = pollItems.firstIndex(of: item) else { return }
-//
-// if index == pollItems.count - 1 {
-// // is the last
-// viewModel.createNewPollOptionIfPossible()
-// DispatchQueue.main.async {
-// self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
-// }
-// } else {
-// // not the last
-// let indexAfter = pollItems.index(after: index)
-// let itemAfter = pollItems[indexAfter]
-// let cell = pollOptionCollectionViewCell(of: itemAfter)
-// cell?.pollOptionView.optionTextField.becomeFirstResponder()
-// }
-// }
-//
-//}
-//
-//// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate
-//extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate {
-// func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) {
-// viewModel.createNewPollOptionIfPossible()
-// DispatchQueue.main.async {
-// self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
-// }
-// }
-//}
-//
-//// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
-//extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
-// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) {
-// viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
-// }
-//}
-//
-//// MARK: - ComposeStatusContentTableViewCellDelegate
-//extension ComposeViewController: ComposeStatusContentTableViewCellDelegate {
-// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool {
-// setupInputAssistantItem(item: textView.inputAssistantItem)
-// return true
-// }
-//}
-//
-//// MARK: - AutoCompleteViewControllerDelegate
-//extension ComposeViewController: AutoCompleteViewControllerDelegate {
-// func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) {
-// guard let info = viewModel.autoCompleteInfo else { return }
-// let _replacedText: String? = {
-// var text: String
-// switch item {
-// case .hashtag(let hashtag):
-// text = "#" + hashtag.name
-// case .hashtagV1(let hashtagName):
-// text = "#" + hashtagName
-// case .account(let account):
-// text = "@" + account.acct
-// case .emoji(let emoji):
-// text = ":" + emoji.shortcode + ":"
-// case .bottomLoader:
-// return nil
-// }
-// return text
-// }()
-// guard let replacedText = _replacedText else { return }
-// guard let text = textEditorView.textView.text else { return }
-//
-// let range = NSRange(info.toHighlightEndRange, in: text)
-// textEditorView.textStorage.replaceCharacters(in: range, with: replacedText)
-// DispatchQueue.main.async {
-// self.textEditorView.textView.insertText(" ") // trigger textView delegate update
-// }
-// viewModel.autoCompleteInfo = nil
-//
-// switch item {
-// case .emoji, .bottomLoader:
-// break
-// default:
-// // set selected range except emoji
-// let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
-// guard textEditorView.textStorage.length <= newRange.location else { return }
-// textEditorView.textView.selectedRange = newRange
-// }
-// }
-//}
-//
-//extension ComposeViewController {
-// override var keyCommands: [UIKeyCommand]? {
-// composeKeyCommands
-// }
-//}
-//
-//extension ComposeViewController {
-//
-// enum ComposeKeyCommand: String, CaseIterable {
-// case discardPost
-// case publishPost
-// case mediaBrowse
-// case mediaPhotoLibrary
-// case mediaCamera
-// case togglePoll
-// case toggleContentWarning
-// case selectVisibilityPublic
-// // TODO: remove selectVisibilityUnlisted from codebase
-// // case selectVisibilityUnlisted
-// case selectVisibilityPrivate
-// case selectVisibilityDirect
-//
-// var title: String {
-// switch self {
-// case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost
-// case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost
-// case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse)
-// case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary)
-// case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera)
-// case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll
-// case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning
-// case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
-// // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
-// case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
-// case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
-// }
-// }
-//
-// // UIKeyCommand input
-// var input: String {
-// switch self {
-// case .discardPost: return "w" // + command
-// case .publishPost: return "\r" // (enter) + command
-// case .mediaBrowse: return "b" // + option + command
-// case .mediaPhotoLibrary: return "p" // + option + command
-// case .mediaCamera: return "c" // + option + command
-// case .togglePoll: return "p" // + shift + command
-// case .toggleContentWarning: return "c" // + shift + command
-// case .selectVisibilityPublic: return "1" // + command
-// // case .selectVisibilityUnlisted: return "2" // + command
-// case .selectVisibilityPrivate: return "2" // + command
-// case .selectVisibilityDirect: return "3" // + command
-// }
-// }
-//
-// var modifierFlags: UIKeyModifierFlags {
-// switch self {
-// case .discardPost: return [.command]
-// case .publishPost: return [.command]
-// case .mediaBrowse: return [.alternate, .command]
-// case .mediaPhotoLibrary: return [.alternate, .command]
-// case .mediaCamera: return [.alternate, .command]
-// case .togglePoll: return [.shift, .command]
-// case .toggleContentWarning: return [.shift, .command]
-// case .selectVisibilityPublic: return [.command]
-// // case .selectVisibilityUnlisted: return [.command]
-// case .selectVisibilityPrivate: return [.command]
-// case .selectVisibilityDirect: return [.command]
-// }
-// }
-//
-// var propertyList: Any {
-// return rawValue
-// }
-// }
-//
-// var composeKeyCommands: [UIKeyCommand]? {
-// ComposeKeyCommand.allCases.map { command in
-// UIKeyCommand(
-// title: command.title,
-// image: nil,
-// action: #selector(Self.composeKeyCommandHandler(_:)),
-// input: command.input,
-// modifierFlags: command.modifierFlags,
-// propertyList: command.propertyList,
-// alternates: [],
-// discoverabilityTitle: nil,
-// attributes: [],
-// state: .off
-// )
-// }
-// }
-//
-// @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) {
-// guard let rawValue = sender.propertyList as? String,
-// let command = ComposeKeyCommand(rawValue: rawValue) else { return }
-//
-// switch command {
-// case .discardPost:
-// cancelBarButtonItemPressed(cancelBarButtonItem)
-// case .publishPost:
-// publishBarButtonItemPressed(publishBarButtonItem)
-// case .mediaBrowse:
-// present(documentPickerController, animated: true, completion: nil)
-// case .mediaPhotoLibrary:
-// present(photoLibraryPicker, animated: true, completion: nil)
-// case .mediaCamera:
-// guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
-// return
-// }
-// present(imagePickerController, animated: true, completion: nil)
-// case .togglePoll:
-// composeToolbarView.pollButton.sendActions(for: .touchUpInside)
-// case .toggleContentWarning:
-// composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside)
-// case .selectVisibilityPublic:
-// viewModel.selectedStatusVisibility = .public
-// // case .selectVisibilityUnlisted:
-// // viewModel.selectedStatusVisibility.value = .unlisted
-// case .selectVisibilityPrivate:
-// viewModel.selectedStatusVisibility = .private
-// case .selectVisibilityDirect:
-// viewModel.selectedStatusVisibility = .direct
-// }
-// }
-//
-//}
+extension ComposeViewController {
+ override var keyCommands: [UIKeyCommand]? {
+ composeKeyCommands
+ }
+}
+
+extension ComposeViewController {
+
+ enum ComposeKeyCommand: String, CaseIterable {
+ case discardPost
+ case publishPost
+ case mediaBrowse
+ case mediaPhotoLibrary
+ case mediaCamera
+ case togglePoll
+ case toggleContentWarning
+ case selectVisibilityPublic
+ // TODO: remove selectVisibilityUnlisted from codebase
+ // case selectVisibilityUnlisted
+ case selectVisibilityPrivate
+ case selectVisibilityDirect
+
+ var title: String {
+ switch self {
+ case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost
+ case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost
+ case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse)
+ case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary)
+ case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera)
+ case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll
+ case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning
+ case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
+ // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
+ case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
+ case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
+ }
+ }
+
+ // UIKeyCommand input
+ var input: String {
+ switch self {
+ case .discardPost: return "w" // + command
+ case .publishPost: return "\r" // (enter) + command
+ case .mediaBrowse: return "b" // + option + command
+ case .mediaPhotoLibrary: return "p" // + option + command
+ case .mediaCamera: return "c" // + option + command
+ case .togglePoll: return "p" // + shift + command
+ case .toggleContentWarning: return "c" // + shift + command
+ case .selectVisibilityPublic: return "1" // + command
+ // case .selectVisibilityUnlisted: return "2" // + command
+ case .selectVisibilityPrivate: return "2" // + command
+ case .selectVisibilityDirect: return "3" // + command
+ }
+ }
+
+ var modifierFlags: UIKeyModifierFlags {
+ switch self {
+ case .discardPost: return [.command]
+ case .publishPost: return [.command]
+ case .mediaBrowse: return [.alternate, .command]
+ case .mediaPhotoLibrary: return [.alternate, .command]
+ case .mediaCamera: return [.alternate, .command]
+ case .togglePoll: return [.shift, .command]
+ case .toggleContentWarning: return [.shift, .command]
+ case .selectVisibilityPublic: return [.command]
+ // case .selectVisibilityUnlisted: return [.command]
+ case .selectVisibilityPrivate: return [.command]
+ case .selectVisibilityDirect: return [.command]
+ }
+ }
+
+ var propertyList: Any {
+ return rawValue
+ }
+ }
+
+ var composeKeyCommands: [UIKeyCommand]? {
+ ComposeKeyCommand.allCases.map { command in
+ UIKeyCommand(
+ title: command.title,
+ image: nil,
+ action: #selector(Self.composeKeyCommandHandler(_:)),
+ input: command.input,
+ modifierFlags: command.modifierFlags,
+ propertyList: command.propertyList,
+ alternates: [],
+ discoverabilityTitle: nil,
+ attributes: [],
+ state: .off
+ )
+ }
+ }
+
+ @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) {
+ guard let rawValue = sender.propertyList as? String,
+ let command = ComposeKeyCommand(rawValue: rawValue) else { return }
+
+ switch command {
+ case .discardPost:
+ cancelBarButtonItemPressed(cancelBarButtonItem)
+ case .publishPost:
+ publishBarButtonItemPressed(publishBarButtonItem)
+ case .mediaBrowse:
+ guard !isViewControllerIsAlreadyModal(composeContentViewController.documentPickerController) else { return }
+ present(composeContentViewController.documentPickerController, animated: true, completion: nil)
+ case .mediaPhotoLibrary:
+ guard !isViewControllerIsAlreadyModal(composeContentViewController.photoLibraryPicker) else { return }
+ present(composeContentViewController.photoLibraryPicker, animated: true, completion: nil)
+ case .mediaCamera:
+ guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
+ return
+ }
+ guard !isViewControllerIsAlreadyModal(composeContentViewController.imagePickerController) else { return }
+ present(composeContentViewController.imagePickerController, animated: true, completion: nil)
+ case .togglePoll:
+ composeContentViewModel.isPollActive.toggle()
+ case .toggleContentWarning:
+ composeContentViewModel.isContentWarningActive.toggle()
+ case .selectVisibilityPublic:
+ composeContentViewModel.visibility = .public
+ // case .selectVisibilityUnlisted:
+ // viewModel.selectedStatusVisibility.value = .unlisted
+ case .selectVisibilityPrivate:
+ composeContentViewModel.visibility = .private
+ case .selectVisibilityDirect:
+ composeContentViewModel.visibility = .direct
+ }
+ }
+
+ private func isViewControllerIsAlreadyModal(_ viewController: UIViewController) -> Bool {
+ return viewController.presentingViewController != nil
+ }
+
+}
diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift
deleted file mode 100644
index b3d8f52dc..000000000
--- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift
+++ /dev/null
@@ -1,490 +0,0 @@
-//
-// ComposeViewModel+Diffable.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-3-11.
-//
-
-import os.log
-import UIKit
-import Combine
-import CoreDataStack
-import MetaTextKit
-import MastodonMeta
-import MastodonAsset
-import MastodonCore
-import MastodonLocalization
-import MastodonSDK
-
-extension ComposeViewModel {
-
-// func setupDataSource(
-// tableView: UITableView,
-// metaTextDelegate: MetaTextDelegate,
-// metaTextViewDelegate: UITextViewDelegate,
-// customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
-// composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
-// composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
-// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
-// composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
-// ) {
-// // UI
-// bind()
-//
-// // content
-// bind(cell: composeStatusContentTableViewCell, tableView: tableView)
-// composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
-// composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
-//
-// // attachment
-// bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
-// composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
-//
-// // poll
-// bind(cell: composeStatusPollTableViewCell, tableView: tableView)
-// composeStatusPollTableViewCell.delegate = self
-// composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
-// composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
-// composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
-// composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate
-//
-// // setup data source
-// tableView.dataSource = self
-// }
-//
-// func setupCustomEmojiPickerDiffableDataSource(
-// for collectionView: UICollectionView,
-// dependency: NeedsDependency
-// ) {
-// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
-// for: collectionView,
-// dependency: dependency
-// )
-// self.customEmojiPickerDiffableDataSource = diffableDataSource
-//
-// let _domain = customEmojiViewModel?.domain
-// customEmojiViewModel?.emojis
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self, weak diffableDataSource] emojis in
-// guard let _ = self else { return }
-// guard let diffableDataSource = diffableDataSource else { return }
-//
-// var snapshot = NSDiffableDataSourceSnapshot()
-// let domain = _domain?.uppercased() ?? " "
-// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
-// snapshot.appendSections([customEmojiSection])
-// let items: [CustomEmojiPickerItem] = {
-// var items = [CustomEmojiPickerItem]()
-// for emoji in emojis where emoji.visibleInPicker {
-// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
-// let item = CustomEmojiPickerItem.emoji(attribute: attribute)
-// items.append(item)
-// }
-// return items
-// }()
-// snapshot.appendItems(items, toSection: customEmojiSection)
-//
-// diffableDataSource.apply(snapshot)
-// }
-// .store(in: &disposeBag)
-// }
-
-}
-
-//// MARK: - UITableViewDataSource
-//extension ComposeViewModel: UITableViewDataSource {
-
-// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-// switch Section.allCases[indexPath.section] {
-// case .repliedTo:
-// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
-// guard case let .reply(record) = composeKind else { return cell }
-//
-// // bind frame publisher
-// cell.framePublisher
-// .receive(on: DispatchQueue.main)
-// .assign(to: \.repliedToCellFrame, on: self)
-// .store(in: &cell.disposeBag)
-//
-// // set initial width
-// if cell.statusView.frame.width == .zero {
-// cell.statusView.frame.size.width = tableView.frame.width
-// }
-//
-// // configure status
-// context.managedObjectContext.performAndWait {
-// guard let replyTo = record.object(in: context.managedObjectContext) else { return }
-// cell.statusView.configure(status: replyTo)
-// }
-//
-// return cell
-// case .status:
-// return composeStatusContentTableViewCell
-// case .attachment:
-// return composeStatusAttachmentTableViewCell
-// case .poll:
-// return composeStatusPollTableViewCell
-// }
-// }
-//}
-
-//// MARK: - ComposeStatusPollTableViewCellDelegate
-//extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
-// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-//
-// self.pollOptionAttributes = options
-// }
-//}
-//
-//extension ComposeViewModel {
-// private func bind() {
-// $isCustomEmojiComposing
-// .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
-// .store(in: &disposeBag)
-//
-// $isContentWarningComposing
-// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
-// .store(in: &disposeBag)
-//
-// // bind compose toolbar UI state
-// Publishers.CombineLatest(
-// $isPollComposing,
-// $attachmentServices
-// )
-// .receive(on: DispatchQueue.main)
-// .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
-// guard let self = self else { return }
-// let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
-// let shouldPollDisable = attachmentServices.count > 0
-//
-// self.isMediaToolbarButtonEnabled = !shouldMediaDisable
-// self.isPollToolbarButtonEnabled = !shouldPollDisable
-// })
-// .store(in: &disposeBag)
-//
-// // calculate `Idempotency-Key`
-// let content = Publishers.CombineLatest3(
-// composeStatusAttribute.$isContentWarningComposing,
-// composeStatusAttribute.$contentWarningContent,
-// composeStatusAttribute.$composeContent
-// )
-// .map { isContentWarningComposing, contentWarningContent, composeContent -> String in
-// if isContentWarningComposing {
-// return contentWarningContent + (composeContent ?? "")
-// } else {
-// return composeContent ?? ""
-// }
-// }
-// let attachmentIDs = $attachmentServices.map { attachments -> String in
-// let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
-// return attachmentIDs.joined(separator: ",")
-// }
-// let pollOptionsAndDuration = Publishers.CombineLatest3(
-// $isPollComposing,
-// $pollOptionAttributes,
-// pollExpiresOptionAttribute.expiresOption
-// )
-// .map { isPollComposing, pollOptionAttributes, expiresOption -> String in
-// guard isPollComposing else {
-// return ""
-// }
-//
-// let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
-// return pollOptions + expiresOption.rawValue
-// }
-//
-// Publishers.CombineLatest4(
-// content,
-// attachmentIDs,
-// pollOptionsAndDuration,
-// $selectedStatusVisibility
-// )
-// .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
-// var hasher = Hasher()
-// hasher.combine(content)
-// hasher.combine(attachmentIDs)
-// hasher.combine(pollOptionsAndDuration)
-// hasher.combine(selectedStatusVisibility.visibility.rawValue)
-// let hashValue = hasher.finalize()
-// return "\(hashValue)"
-// }
-// .assign(to: \.value, on: idempotencyKey)
-// .store(in: &disposeBag)
-//
-// // bind modal dismiss state
-// composeStatusAttribute.$composeContent
-// .receive(on: DispatchQueue.main)
-// .map { [weak self] content in
-// let content = content ?? ""
-// if content.isEmpty {
-// return true
-// }
-// // if preInsertedContent plus a space is equal to the content, simply dismiss the modal
-// if let preInsertedContent = self?.preInsertedContent {
-// return content == preInsertedContent
-// }
-// return false
-// }
-// .assign(to: &$shouldDismiss)
-//
-// // bind compose bar button item UI state
-// let isComposeContentEmpty = composeStatusAttribute.$composeContent
-// .map { ($0 ?? "").isEmpty }
-// let isComposeContentValid = $characterCount
-// .compactMap { [weak self] characterCount -> Bool in
-// guard let self = self else { return characterCount <= 500 }
-// return characterCount <= self.composeContentLimit
-// }
-// let isMediaEmpty = $attachmentServices
-// .map { $0.isEmpty }
-// let isMediaUploadAllSuccess = $attachmentServices
-// .map { services in
-// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
-// }
-// let isPollAttributeAllValid = $pollOptionAttributes
-// .map { pollAttributes in
-// pollAttributes.allSatisfy { attribute -> Bool in
-// !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
-// }
-// }
-//
-// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
-// isComposeContentEmpty,
-// isComposeContentValid,
-// isMediaEmpty,
-// isMediaUploadAllSuccess
-// )
-// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
-// if isMediaEmpty {
-// return isComposeContentValid && !isComposeContentEmpty
-// } else {
-// return isComposeContentValid && isMediaUploadAllSuccess
-// }
-// }
-// .eraseToAnyPublisher()
-//
-// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
-// isComposeContentEmpty,
-// isComposeContentValid,
-// $isPollComposing,
-// isPollAttributeAllValid
-// )
-// .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
-// if isPollComposing {
-// return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
-// } else {
-// return isComposeContentValid && !isComposeContentEmpty
-// }
-// }
-// .eraseToAnyPublisher()
-//
-// Publishers.CombineLatest(
-// isPublishBarButtonItemEnabledPrecondition1,
-// isPublishBarButtonItemEnabledPrecondition2
-// )
-// .map { $0 && $1 }
-// .assign(to: &$isPublishBarButtonItemEnabled)
-// }
-//}
-//
-//extension ComposeViewModel {
-// private func bind(
-// cell: ComposeStatusContentTableViewCell,
-// tableView: UITableView
-// ) {
-// // bind status content character count
-// Publishers.CombineLatest3(
-// composeStatusAttribute.$composeContent,
-// composeStatusAttribute.$isContentWarningComposing,
-// composeStatusAttribute.$contentWarningContent
-// )
-// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
-// let composeContent = composeContent ?? ""
-// var count = composeContent.count
-// if isContentWarningComposing {
-// count += contentWarningContent.count
-// }
-// return count
-// }
-// .assign(to: &$characterCount)
-//
-// // bind content warning
-// composeStatusAttribute.$isContentWarningComposing
-// .receive(on: DispatchQueue.main)
-// .sink { [weak cell, weak tableView] isContentWarningComposing in
-// guard let cell = cell else { return }
-// guard let tableView = tableView else { return }
-//
-// // self size input cell
-// cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
-// cell.statusContentWarningEditorView.alpha = 0
-// UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
-// cell.statusContentWarningEditorView.alpha = 1
-// tableView.beginUpdates()
-// tableView.endUpdates()
-// } completion: { _ in
-// // do nothing
-// }
-// }
-// .store(in: &disposeBag)
-//
-// cell.contentWarningContent
-// .removeDuplicates()
-// .receive(on: DispatchQueue.main)
-// .sink { [weak tableView, weak self] text in
-// guard let self = self else { return }
-// // bind input data
-// self.composeStatusAttribute.contentWarningContent = text
-//
-// // self size input cell
-// guard let tableView = tableView else { return }
-// UIView.performWithoutAnimation {
-// tableView.beginUpdates()
-// tableView.endUpdates()
-// }
-// }
-// .store(in: &cell.disposeBag)
-//
-// // configure custom emoji picker
-// ComposeStatusSection.configureCustomEmojiPicker(
-// viewModel: customEmojiPickerInputViewModel,
-// customEmojiReplaceableTextInput: cell.metaText.textView,
-// disposeBag: &disposeBag
-// )
-// ComposeStatusSection.configureCustomEmojiPicker(
-// viewModel: customEmojiPickerInputViewModel,
-// customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
-// disposeBag: &disposeBag
-// )
-// }
-//}
-//
-//extension ComposeViewModel {
-// private func bind(
-// cell: ComposeStatusPollTableViewCell,
-// tableView: UITableView
-// ) {
-// Publishers.CombineLatest(
-// $isPollComposing,
-// $pollOptionAttributes
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isPollComposing, pollOptionAttributes in
-// guard let self = self else { return }
-// guard self.isViewAppeared else { return }
-//
-// let cell = self.composeStatusPollTableViewCell
-// guard let dataSource = cell.dataSource else { return }
-//
-// var snapshot = NSDiffableDataSourceSnapshot()
-// snapshot.appendSections([.main])
-// var items: [ComposeStatusPollItem] = []
-// if isPollComposing {
-// for attribute in pollOptionAttributes {
-// items.append(.pollOption(attribute: attribute))
-// }
-// if pollOptionAttributes.count < self.maxPollOptions {
-// items.append(.pollOptionAppendEntry)
-// }
-// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
-// }
-// snapshot.appendItems(items, toSection: .main)
-//
-// tableView.performBatchUpdates {
-// if #available(iOS 15.0, *) {
-// dataSource.apply(snapshot, animatingDifferences: false)
-// } else {
-// dataSource.apply(snapshot, animatingDifferences: true)
-// }
-// }
-// }
-// .store(in: &disposeBag)
-//
-// // bind delegate
-// $pollOptionAttributes
-// .sink { [weak self] pollAttributes in
-// guard let self = self else { return }
-// pollAttributes.forEach { $0.delegate = self }
-// }
-// .store(in: &disposeBag)
-// }
-//}
-//
-//extension ComposeViewModel {
-// private func bind(
-// cell: ComposeStatusAttachmentTableViewCell,
-// tableView: UITableView
-// ) {
-// cell.collectionViewHeightDidUpdate
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] _ in
-// guard let _ = self else { return }
-// tableView.beginUpdates()
-// tableView.endUpdates()
-// }
-// .store(in: &disposeBag)
-//
-// $attachmentServices
-// .removeDuplicates()
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] attachmentServices in
-// guard let self = self else { return }
-// guard self.isViewAppeared else { return }
-//
-// let cell = self.composeStatusAttachmentTableViewCell
-// guard let dataSource = cell.dataSource else { return }
-//
-// var snapshot = NSDiffableDataSourceSnapshot()
-// snapshot.appendSections([.main])
-// let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
-// snapshot.appendItems(items, toSection: .main)
-//
-// if #available(iOS 15.0, *) {
-// dataSource.applySnapshotUsingReloadData(snapshot)
-// } else {
-// dataSource.apply(snapshot, animatingDifferences: false)
-// }
-// }
-// .store(in: &disposeBag)
-//
-// // setup attribute updater
-// $attachmentServices
-// .receive(on: DispatchQueue.main)
-// .debounce(for: 0.3, scheduler: DispatchQueue.main)
-// .sink { attachmentServices in
-// // drive service upload state
-// // make image upload in the queue
-// for attachmentService in attachmentServices {
-// // skip when prefix N task when task finish OR fail OR uploading
-// guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
-// if currentState is MastodonAttachmentService.UploadState.Fail {
-// continue
-// }
-// if currentState is MastodonAttachmentService.UploadState.Finish {
-// continue
-// }
-// if currentState is MastodonAttachmentService.UploadState.Processing {
-// continue
-// }
-// if currentState is MastodonAttachmentService.UploadState.Uploading {
-// break
-// }
-// // trigger uploading one by one
-// if currentState is MastodonAttachmentService.UploadState.Initial {
-// attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
-// break
-// }
-// }
-// }
-// .store(in: &disposeBag)
-//
-// // bind delegate
-// $attachmentServices
-// .sink { [weak self] attachmentServices in
-// guard let self = self else { return }
-// attachmentServices.forEach { $0.delegate = self }
-// }
-// .store(in: &disposeBag)
-// }
-//}
diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
deleted file mode 100644
index b9ed18c45..000000000
--- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift
+++ /dev/null
@@ -1,164 +0,0 @@
-//
-// ComposeViewModel+PublishState.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-3-18.
-//
-
-import os.log
-import Foundation
-import Combine
-import CoreDataStack
-import GameplayKit
-import MastodonSDK
-
-//extension ComposeViewModel {
-// class PublishState: GKState {
-// weak var viewModel: ComposeViewModel?
-//
-// init(viewModel: ComposeViewModel) {
-// 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)
-// viewModel?.publishStateMachinePublisher.value = self
-// }
-// }
-//}
-
-//extension ComposeViewModel.PublishState {
-// class Initial: ComposeViewModel.PublishState {
-// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
-// return stateClass == Publishing.self
-// }
-// }
-//
-// class Publishing: ComposeViewModel.PublishState {
-//
-// var publishingSubscription: AnyCancellable?
-//
-// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
-// return stateClass == Fail.self || stateClass == Finish.self
-// }
-//
-// override func didEnter(from previousState: GKState?) {
-// super.didEnter(from: previousState)
-// guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
-//
-// viewModel.updatePublishDate()
-//
-// let authenticationBox = viewModel.authenticationBox
-// let domain = authenticationBox.domain
-// let attachmentServices = viewModel.attachmentServices
-// let mediaIDs = attachmentServices.compactMap { attachmentService in
-// attachmentService.attachment.value?.id
-// }
-// let pollOptions: [String]? = {
-// guard viewModel.isPollComposing else { return nil }
-// return viewModel.pollOptionAttributes.map { attribute in attribute.option.value }
-// }()
-// let pollExpiresIn: Int? = {
-// guard viewModel.isPollComposing else { return nil }
-// return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds
-// }()
-// let inReplyToID: Mastodon.Entity.Status.ID? = {
-// guard case let .reply(status) = viewModel.composeKind else { return nil }
-// var id: Mastodon.Entity.Status.ID?
-// viewModel.context.managedObjectContext.performAndWait {
-// guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return }
-// id = replyTo.id
-// }
-// return id
-// }()
-// let sensitive: Bool = viewModel.isContentWarningComposing
-// let spoilerText: String? = {
-// let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines)
-// guard !text.isEmpty else {
-// return nil
-// }
-// return text
-// }()
-// let visibility = viewModel.selectedStatusVisibility.visibility
-//
-// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = {
-// var subscriptions: [AnyPublisher, Error>] = []
-// for attachmentService in attachmentServices {
-// guard let attachmentID = attachmentService.attachment.value?.id else { continue }
-// let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
-// guard !description.isEmpty else { continue }
-// let query = Mastodon.API.Media.UpdateMediaQuery(
-// file: nil,
-// thumbnail: nil,
-// description: description,
-// focus: nil
-// )
-// let subscription = viewModel.context.apiService.updateMedia(
-// domain: domain,
-// attachmentID: attachmentID,
-// query: query,
-// mastodonAuthenticationBox: authenticationBox
-// )
-// subscriptions.append(subscription)
-// }
-// return subscriptions
-// }()
-//
-// let idempotencyKey = viewModel.idempotencyKey.value
-//
-// publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions)
-// .collect()
-// .asyncMap { attachments -> Mastodon.Response.Content in
-// let query = Mastodon.API.Statuses.PublishStatusQuery(
-// status: viewModel.composeStatusAttribute.composeContent,
-// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
-// pollOptions: pollOptions,
-// pollExpiresIn: pollExpiresIn,
-// inReplyToID: inReplyToID,
-// sensitive: sensitive,
-// spoilerText: spoilerText,
-// visibility: visibility
-// )
-// return try await viewModel.context.apiService.publishStatus(
-// domain: domain,
-// idempotencyKey: idempotencyKey,
-// query: query,
-// authenticationBox: authenticationBox
-// )
-// }
-// .receive(on: DispatchQueue.main)
-// .sink { completion in
-// switch completion {
-// case .failure(let error):
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
-// stateMachine.enter(Fail.self)
-// case .finished:
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function)
-// stateMachine.enter(Finish.self)
-// }
-// } receiveValue: { response in
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri)
-// }
-// }
-// }
-//
-// class Fail: ComposeViewModel.PublishState {
-// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
-// // allow discard publishing
-// return stateClass == Publishing.self || stateClass == Discard.self
-// }
-// }
-//
-// class Discard: ComposeViewModel.PublishState {
-// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
-// return false
-// }
-// }
-//
-// class Finish: ComposeViewModel.PublishState {
-// override func isValidNextState(_ stateClass: AnyClass) -> Bool {
-// return false
-// }
-// }
-//
-//}
diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift
index 45c9f1e93..bf234b095 100644
--- a/Mastodon/Scene/Compose/ComposeViewModel.swift
+++ b/Mastodon/Scene/Compose/ComposeViewModel.swift
@@ -18,7 +18,7 @@ import MastodonLocalization
import MastodonMeta
import MastodonUI
-final class ComposeViewModel: NSObject {
+final class ComposeViewModel {
let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
@@ -30,91 +30,13 @@ final class ComposeViewModel: NSObject {
let context: AppContext
let authContext: AuthContext
let kind: ComposeContentViewModel.Kind
-
-// var authenticationBox: MastodonAuthenticationBox {
-// authContext.mastodonAuthenticationBox
-// }
-//
-// @Published var isPollComposing = false
-// @Published var isCustomEmojiComposing = false
-// @Published var isContentWarningComposing = false
-//
-// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
-// @Published var repliedToCellFrame: CGRect = .zero
-// @Published var autoCompleteRetryLayoutTimes = 0
-// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil
let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit
-// var isViewAppeared = false
// output
-// let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
-// var composeContentLimit: Int {
-// guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
-// return max(1, maxCharacters)
-// }
-// var maxMediaAttachments: Int {
-// guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
-// return 4
-// }
-// // FIXME: update timeline media preview UI
-// return min(4, max(1, maxMediaAttachments))
-// // return max(1, maxMediaAttachments)
-// }
-// var maxPollOptions: Int {
-// guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
-// return max(2, maxOptions)
-// }
-//
-// let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
-// let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
-// let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
-// let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()
-//
-// // var dataSource: UITableViewDiffableDataSource?
-// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource?
-// private(set) lazy var publishStateMachine: GKStateMachine = {
-// // exclude timeline middle fetcher state
-// let stateMachine = GKStateMachine(states: [
-// PublishState.Initial(viewModel: self),
-// PublishState.Publishing(viewModel: self),
-// PublishState.Fail(viewModel: self),
-// PublishState.Discard(viewModel: self),
-// PublishState.Finish(viewModel: self),
-// ])
-// stateMachine.enter(PublishState.Initial.self)
-// return stateMachine
-// }()
-// private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil)
-// private(set) var publishDate = Date() // update it when enter Publishing state
-//
-// // TODO: group post material into Hashable class
-// var idempotencyKey = CurrentValueSubject(UUID().uuidString)
-//
-// // UI & UX
-// @Published var title: String
-// @Published var shouldDismiss = true
-// @Published var isPublishBarButtonItemEnabled = false
-// @Published var isMediaToolbarButtonEnabled = true
-// @Published var isPollToolbarButtonEnabled = true
-// @Published var characterCount = 0
-// @Published var collectionViewState: CollectionViewState = .fold
-//
-// // for hashtag: "# "
-// // for mention: "@ "
-// var preInsertedContent: String?
-//
-// // custom emojis
-// let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
-// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
-// @Published var isLoadingCustomEmoji = false
-//
-// // attachment
-// @Published var attachmentServices: [MastodonAttachmentService] = []
-//
-// // polls
-// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
-// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
+
+ // UI & UX
+ @Published var title: String
init(
context: AppContext,
@@ -124,63 +46,14 @@ final class ComposeViewModel: NSObject {
self.context = context
self.authContext = authContext
self.kind = kind
+ // end init
-// self.title = {
-// switch composeKind {
-// case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
-// case .reply: return L10n.Scene.Compose.Title.newReply
-// }
-// }()
-// self.selectedStatusVisibility = {
-// // default private when user locked
-// var visibility: ComposeToolbarView.VisibilitySelectionType = {
-// guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
-// else {
-// return .public
-// }
-// return author.locked ? .private : .public
-// }()
-// // set visibility for reply post
-// switch composeKind {
-// case .reply(let record):
-// context.managedObjectContext.performAndWait {
-// guard let status = record.object(in: context.managedObjectContext) else {
-// assertionFailure()
-// return
-// }
-// let repliedStatusVisibility = status.visibility
-// switch repliedStatusVisibility {
-// case .public, .unlisted:
-// // keep default
-// break
-// case .private:
-// visibility = .private
-// case .direct:
-// visibility = .direct
-// case ._other:
-// assertionFailure()
-// break
-// }
-// }
-// default:
-// break
-// }
-// return visibility
-// }()
-// // set limit
-// self.instanceConfiguration = {
-// var configuration: Mastodon.Entity.Instance.Configuration? = nil
-// context.managedObjectContext.performAndWait {
-// guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return }
-// configuration = authentication.instance?.configuration
-// }
-// return configuration
-// }()
-// self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain)
-// super.init()
-// // end init
-//
-// setup(cell: composeStatusContentTableViewCell)
+ self.title = {
+ switch kind {
+ case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost
+ case .reply: return L10n.Scene.Compose.Title.newReply
+ }
+ }()
}
deinit {
@@ -188,194 +61,3 @@ final class ComposeViewModel: NSObject {
}
}
-
-extension ComposeViewModel {
-// func createNewPollOptionIfPossible() {
-// guard pollOptionAttributes.count < maxPollOptions else { return }
-//
-// let attribute = ComposeStatusPollItem.PollOptionAttribute()
-// pollOptionAttributes = pollOptionAttributes + [attribute]
-// }
-//
-// func updatePublishDate() {
-// publishDate = Date()
-// }
-}
-
-//extension ComposeViewModel {
-//
-// enum AttachmentPrecondition: Error, LocalizedError {
-// case videoAttachWithPhoto
-// case moreThanOneVideo
-//
-// var errorDescription: String? {
-// return L10n.Common.Alerts.PublishPostFailure.title
-// }
-//
-// var failureReason: String? {
-// switch self {
-// case .videoAttachWithPhoto:
-// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
-// case .moreThanOneVideo:
-// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
-// }
-// }
-// }
-//
-// // check exclusive limit:
-// // - up to 1 video
-// // - up to N photos
-// func checkAttachmentPrecondition() throws {
-// let attachmentServices = self.attachmentServices
-// guard !attachmentServices.isEmpty else { return }
-// var photoAttachmentServices: [MastodonAttachmentService] = []
-// var videoAttachmentServices: [MastodonAttachmentService] = []
-// attachmentServices.forEach { service in
-// guard let file = service.file.value else {
-// assertionFailure()
-// return
-// }
-// switch file {
-// case .jpeg, .png, .gif:
-// photoAttachmentServices.append(service)
-// case .other:
-// videoAttachmentServices.append(service)
-// }
-// }
-//
-// if !videoAttachmentServices.isEmpty {
-// guard videoAttachmentServices.count == 1 else {
-// throw AttachmentPrecondition.moreThanOneVideo
-// }
-// guard photoAttachmentServices.isEmpty else {
-// throw AttachmentPrecondition.videoAttachWithPhoto
-// }
-// }
-// }
-//
-//}
-//
-//// MARK: - MastodonAttachmentServiceDelegate
-//extension ComposeViewModel: MastodonAttachmentServiceDelegate {
-// func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
-// // trigger new output event
-// attachmentServices = attachmentServices
-// }
-//}
-//
-//// MARK: - ComposePollAttributeDelegate
-//extension ComposeViewModel: ComposePollAttributeDelegate {
-// func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
-// // trigger update
-// pollOptionAttributes = pollOptionAttributes
-// }
-//}
-//
-//extension ComposeViewModel {
-// private func setup(
-// cell: ComposeStatusContentTableViewCell
-// ) {
-// setupStatusHeader(cell: cell)
-// setupStatusAuthor(cell: cell)
-// setupStatusContent(cell: cell)
-// }
-//
-// private func setupStatusHeader(
-// cell: ComposeStatusContentTableViewCell
-// ) {
-// // configure header
-// let managedObjectContext = context.managedObjectContext
-// managedObjectContext.performAndWait {
-// guard case let .reply(record) = self.composeKind,
-// let replyTo = record.object(in: managedObjectContext)
-// else {
-// cell.statusView.viewModel.header = .none
-// return
-// }
-//
-// let info: StatusView.ViewModel.Header.ReplyInfo
-// do {
-// let content = MastodonContent(
-// content: replyTo.author.displayNameWithFallback,
-// emojis: replyTo.author.emojis.asDictionary
-// )
-// let metaContent = try MastodonMetaContent.convert(document: content)
-// info = .init(header: metaContent)
-// } catch {
-// let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback)
-// info = .init(header: metaContent)
-// }
-// cell.statusView.viewModel.header = .reply(info: info)
-// }
-// }
-//
-// private func setupStatusAuthor(
-// cell: ComposeStatusContentTableViewCell
-// ) {
-// self.context.managedObjectContext.performAndWait {
-// guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
-// cell.statusView.configureAuthor(author: author)
-// }
-// }
-//
-// private func setupStatusContent(
-// cell: ComposeStatusContentTableViewCell
-// ) {
-// switch composeKind {
-// case .reply(let record):
-// context.managedObjectContext.performAndWait {
-// guard let status = record.object(in: context.managedObjectContext) else { return }
-// let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
-//
-// var mentionAccts: [String] = []
-// if author?.id != status.author.id {
-// mentionAccts.append("@" + status.author.acct)
-// }
-// let mentions = status.mentions
-// .filter { author?.id != $0.id }
-// for mention in mentions {
-// let acct = "@" + mention.acct
-// guard !mentionAccts.contains(acct) else { continue }
-// mentionAccts.append(acct)
-// }
-// for acct in mentionAccts {
-// UITextChecker.learnWord(acct)
-// }
-// if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
-// self.isContentWarningComposing = true
-// self.composeStatusAttribute.contentWarningContent = spoilerText
-// }
-//
-// let initialComposeContent = mentionAccts.joined(separator: " ")
-// let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
-// self.preInsertedContent = preInsertedContent
-// self.composeStatusAttribute.composeContent = preInsertedContent
-// }
-// case .hashtag(let hashtag):
-// let initialComposeContent = "#" + hashtag
-// UITextChecker.learnWord(initialComposeContent)
-// let preInsertedContent = initialComposeContent + " "
-// self.preInsertedContent = preInsertedContent
-// self.composeStatusAttribute.composeContent = preInsertedContent
-// case .mention(let record):
-// context.managedObjectContext.performAndWait {
-// guard let user = record.object(in: context.managedObjectContext) else { return }
-// let initialComposeContent = "@" + user.acct
-// UITextChecker.learnWord(initialComposeContent)
-// let preInsertedContent = initialComposeContent + " "
-// self.preInsertedContent = preInsertedContent
-// self.composeStatusAttribute.composeContent = preInsertedContent
-// }
-// case .post:
-// self.preInsertedContent = nil
-// }
-//
-// // configure content warning
-// if let composeContent = composeStatusAttribute.composeContent {
-// cell.metaText.textView.text = composeContent
-// }
-//
-// // configure content warning
-// cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent
-// }
-//}
diff --git a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift
index ed6f68fec..1ed76a485 100644
--- a/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift
+++ b/Mastodon/Scene/Profile/About/Cell/ProfileFieldCollectionViewCell.swift
@@ -26,6 +26,16 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell {
let keyMetaLabel = MetaLabel(style: .profileFieldName)
let valueMetaLabel = MetaLabel(style: .profileFieldValue)
+ let checkmark = UIImageView(image: Asset.Editing.checkmark.image.withRenderingMode(.alwaysTemplate))
+ var checkmarkPopoverString: String? = nil;
+ let tapGesture = UITapGestureRecognizer();
+ private var _editMenuInteraction: Any? = nil
+ @available(iOS 16, *)
+ fileprivate var editMenuInteraction: UIEditMenuInteraction {
+ _editMenuInteraction = _editMenuInteraction ?? UIEditMenuInteraction(delegate: self)
+ return _editMenuInteraction as! UIEditMenuInteraction
+ }
+
override func prepareForReuse() {
super.prepareForReuse()
@@ -47,6 +57,17 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell {
extension ProfileFieldCollectionViewCell {
private func _init() {
+ // Setup colors
+ checkmark.tintColor = Asset.Scene.Profile.About.bioAboutFieldVerifiedCheckmark.color;
+
+ // Setup gestures
+ tapGesture.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.didTapCheckmark(_:)))
+ checkmark.addGestureRecognizer(tapGesture)
+ checkmark.isUserInteractionEnabled = true
+ if #available(iOS 16, *) {
+ checkmark.addInteraction(editMenuInteraction)
+ }
+
// containerStackView: V - [ metaContainer | plainContainer ]
let containerStackView = UIStackView()
containerStackView.axis = .vertical
@@ -63,19 +84,62 @@ extension ProfileFieldCollectionViewCell {
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 11),
])
- // metaContainer: V - [ keyMetaLabel | valueMetaLabel ]
+ // metaContainer: V - [ keyMetaLabel | valueContainer ]
let metaContainer = UIStackView()
metaContainer.axis = .vertical
metaContainer.spacing = 2
containerStackView.addArrangedSubview(metaContainer)
+ // valueContainer: H - [ valueMetaLabel | checkmark ]
+ let valueContainer = UIStackView()
+ valueContainer.axis = .horizontal
+ valueContainer.spacing = 2
+
metaContainer.addArrangedSubview(keyMetaLabel)
- metaContainer.addArrangedSubview(valueMetaLabel)
+ valueContainer.addArrangedSubview(valueMetaLabel)
+ valueContainer.addArrangedSubview(checkmark)
+ metaContainer.addArrangedSubview(valueContainer)
keyMetaLabel.linkDelegate = self
valueMetaLabel.linkDelegate = self
}
+ @objc public func didTapCheckmark(_ recognizer: UITapGestureRecognizer) {
+ if #available(iOS 16, *) {
+ editMenuInteraction.presentEditMenu(with: UIEditMenuConfiguration(identifier: nil, sourcePoint: recognizer.location(in: checkmark)))
+ } else {
+ guard let editMenuLabel = checkmarkPopoverString else { return }
+
+ self.isUserInteractionEnabled = true
+ self.becomeFirstResponder()
+
+ UIMenuController.shared.menuItems = [
+ UIMenuItem(
+ title: editMenuLabel,
+ action: #selector(dismissVerifiedMenu)
+ )
+ ]
+ UIMenuController.shared.showMenu(from: checkmark, rect: checkmark.bounds)
+ }
+ }
+}
+
+// UIMenuController boilerplate
+@available(iOS, deprecated: 16, message: "Can be removed when target version is >=16 -- boilerplate to maintain compatibility with UIMenuController")
+extension ProfileFieldCollectionViewCell {
+ override var canBecomeFirstResponder: Bool { true }
+
+ override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+ if action == #selector(dismissVerifiedMenu) {
+ return true
+ }
+
+ return super.canPerformAction(action, withSender: sender)
+ }
+
+ @objc public func dismissVerifiedMenu() {
+ UIMenuController.shared.hideMenu()
+ }
}
// MARK: - MetaLabelDelegate
@@ -85,3 +149,16 @@ extension ProfileFieldCollectionViewCell: MetaLabelDelegate {
delegate?.profileFieldCollectionViewCell(self, metaLebel: metaLabel, didSelectMeta: meta)
}
}
+
+// MARK: UIEditMenuInteractionDelegate
+@available(iOS 16.0, *)
+extension ProfileFieldCollectionViewCell: UIEditMenuInteractionDelegate {
+ func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
+ guard let editMenuLabel = checkmarkPopoverString else { return UIMenu(children: []) }
+ return UIMenu(children: [UIAction(title: editMenuLabel) { _ in return }])
+ }
+
+ func editMenuInteraction(_ interaction: UIEditMenuInteraction, targetRectFor configuration: UIEditMenuConfiguration) -> CGRect {
+ return checkmark.frame
+ }
+}
diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
index 68a3d0fea..044894b8a 100644
--- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
+++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
@@ -52,7 +52,7 @@ final class ProfileAboutViewModel {
$emojiMeta
)
.map { fields, emojiMeta in
- fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, emojiMeta: emojiMeta) }
+ fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, verifiedAt: $0.verifiedAt, emojiMeta: emojiMeta) }
}
.assign(to: &profileInfo.$fields)
@@ -72,6 +72,7 @@ final class ProfileAboutViewModel {
ProfileFieldItem.FieldValue(
name: field.name,
value: field.value,
+ verifiedAt: field.verifiedAt,
emojiMeta: [:] // no use for editing
)
} ?? []
@@ -92,7 +93,7 @@ extension ProfileAboutViewModel {
func appendFieldItem() {
var fields = profileInfoEditing.fields
guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return }
- fields.append(ProfileFieldItem.FieldValue(name: "", value: "", emojiMeta: [:]))
+ fields.append(ProfileFieldItem.FieldValue(name: "", value: "", verifiedAt: nil, emojiMeta: [:]))
profileInfoEditing.fields = fields
}
@@ -112,7 +113,7 @@ extension ProfileAboutViewModel: ProfileViewModelEditable {
let isFieldsEqual: Bool = {
let originalFields = self.accountForEdit?.source?.fields?.compactMap { field in
- ProfileFieldItem.FieldValue(name: field.name, value: field.value, emojiMeta: [:])
+ ProfileFieldItem.FieldValue(name: field.name, value: field.value, verifiedAt: nil, emojiMeta: [:])
} ?? []
let editFields = profileInfoEditing.fields
guard editFields.count == originalFields.count else { return false }
diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift
index 1b0d505b5..f96a02ddc 100644
--- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift
+++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel.swift
@@ -7,7 +7,6 @@
import Foundation
import Combine
-import Combine
import CoreData
import CoreDataStack
import GameplayKit
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index 3ce1fd33a..8749bae9c 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -51,6 +51,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
action: #selector(ProfileViewController.settingBarButtonItemPressed(_:))
)
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.settings
return barButtonItem
}()
@@ -62,6 +63,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
action: #selector(ProfileViewController.shareBarButtonItemPressed(_:))
)
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.share
return barButtonItem
}()
@@ -73,6 +75,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
action: #selector(ProfileViewController.favoriteBarButtonItemPressed(_:))
)
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Scene.Favorite.title
return barButtonItem
}()
@@ -84,18 +87,21 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
action: #selector(ProfileViewController.bookmarkBarButtonItemPressed(_:))
)
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Scene.Bookmark.title
return barButtonItem
}()
private(set) lazy var replyBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrowshape.turn.up.left"), style: .plain, target: self, action: #selector(ProfileViewController.replyBarButtonItemPressed(_:)))
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.reply
return barButtonItem
}()
let moreMenuBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
+ barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.seeMore
return barButtonItem
}()
@@ -552,6 +558,9 @@ extension ProfileViewController {
userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
+ // trigger authenticated user account update
+ viewModel.context.authenticationService.updateActiveUserAccountPublisher.send()
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
sender.endRefreshing()
}
diff --git a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift
index 75021934b..361f5db24 100644
--- a/Mastodon/Scene/Report/ReportResult/ReportResultView.swift
+++ b/Mastodon/Scene/Report/ReportResult/ReportResultView.swift
@@ -8,7 +8,6 @@
import UIKit
import SwiftUI
import MastodonSDK
-import MastodonUI
import MastodonAsset
import MastodonCore
import MastodonUI
diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
index 4f6b21d96..ca301a535 100644
--- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
+++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift
@@ -312,7 +312,12 @@ extension MainTabBarController {
guard let profileTabItem = _profileTabItem else { return }
let currentUserDisplayName = user.displayNameWithFallback ?? "no user"
profileTabItem.accessibilityHint = L10n.Scene.AccountList.tabBarHint(currentUserDisplayName)
-
+
+ context.authenticationService.updateActiveUserAccountPublisher
+ .sink { [weak self] in
+ self?.updateUserAccount()
+ }
+ .store(in: &disposeBag)
} else {
self.avatarURLObserver = nil
}
@@ -487,6 +492,26 @@ extension MainTabBarController {
avatarButton.setNeedsLayout()
}
+ private func updateUserAccount() {
+ guard let authContext = authContext else { return }
+
+ Task { @MainActor in
+ let profileResponse = try await context.apiService.authenticatedUserInfo(
+ authenticationBox: authContext.mastodonAuthenticationBox
+ )
+
+ if let user = authContext.mastodonAuthenticationBox.authenticationRecord.object(
+ in: context.managedObjectContext
+ )?.user {
+ user.update(
+ property: .init(
+ entity: profileResponse.value,
+ domain: authContext.mastodonAuthenticationBox.domain
+ )
+ )
+ }
+ }
+ }
}
extension MainTabBarController {
diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift
index b25f7c5eb..4476477fd 100644
--- a/Mastodon/Supporting Files/SceneDelegate.swift
+++ b/Mastodon/Supporting Files/SceneDelegate.swift
@@ -109,6 +109,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// trigger status filter update
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
+
+ // trigger authenticated user account update
+ AppContext.shared.authenticationService.updateActiveUserAccountPublisher.send()
if let shortcutItem = savedShortCutItem {
Task {
diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift
index ca241038b..7a4201894 100644
--- a/MastodonSDK/Package.swift
+++ b/MastodonSDK/Package.swift
@@ -25,9 +25,9 @@ let package = Package(
],
dependencies: [
.package(name: "ArkanaKeys", path: "../dependencies/ArkanaKeys"),
- .package(name: "FaviconFinder", url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"),
- .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"),
- .package(name: "UITextView+Placeholder", url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"),
+ .package(url: "https://github.com/will-lumley/FaviconFinder.git", from: "3.2.2"),
+ .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3"),
+ .package(url: "https://github.com/MainasuK/UITextView-Placeholder.git", from: "1.4.1"),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"),
.package(url: "https://github.com/Alamofire/AlamofireImage.git", from: "4.1.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"),
@@ -49,6 +49,7 @@ let package = Package(
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.12.0"),
.package(url: "https://github.com/eneko/Stripes.git", from: "0.2.0"),
.package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.4.1"),
+ .package(url: "https://github.com/NextLevel/NextLevelSessionExporter.git", from: "0.4.6"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -112,8 +113,8 @@ let package = Package(
.product(name: "FLAnimatedImage", package: "FLAnimatedImage"),
.product(name: "FaviconFinder", package: "FaviconFinder"),
.product(name: "Nuke", package: "Nuke"),
- .product(name: "Introspect", package: "Introspect"),
- .product(name: "UITextView+Placeholder", package: "UITextView+Placeholder"),
+ .product(name: "Introspect", package: "SwiftUI-Introspect"),
+ .product(name: "UITextView+Placeholder", package: "UITextView-Placeholder"),
.product(name: "UIHostingConfigurationBackport", package: "UIHostingConfigurationBackport"),
.product(name: "TabBarPager", package: "TabBarPager"),
.product(name: "ThirdPartyMailer", package: "ThirdPartyMailer"),
@@ -124,6 +125,7 @@ let package = Package(
.product(name: "PanModal", package: "PanModal"),
.product(name: "Stripes", package: "Stripes"),
.product(name: "Kingfisher", package: "Kingfisher"),
+ .product(name: "NextLevelSessionExporter", package: "NextLevelSessionExporter"),
]
),
.testTarget(
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json
new file mode 100644
index 000000000..7a1c8d9e2
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/indicator.button.background.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.400",
+ "green" : "0.275",
+ "red" : "0.275"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.400",
+ "green" : "0.275",
+ "red" : "0.275"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf
new file mode 100644
index 000000000..a15c522d8
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Arrow Clockwise.pdf
@@ -0,0 +1,91 @@
+%PDF-1.7
+
+1 0 obj
+ << >>
+endobj
+
+2 0 obj
+ << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 2.750000 2.750000 cm
+0.000000 0.000000 0.000000 scn
+9.250000 16.500000 m
+5.245935 16.500000 2.000000 13.254065 2.000000 9.250000 c
+2.000000 5.245935 5.245935 2.000000 9.250000 2.000000 c
+13.254065 2.000000 16.500000 5.245935 16.500000 9.250000 c
+16.500000 9.535608 16.483484 9.817360 16.451357 10.094351 c
+16.383255 10.681498 16.809317 11.250000 17.400400 11.250000 c
+17.916018 11.250000 18.369314 10.891933 18.431660 10.380100 c
+18.476776 10.009713 18.500000 9.632568 18.500000 9.250000 c
+18.500000 4.141366 14.358634 0.000000 9.250000 0.000000 c
+4.141366 0.000000 0.000000 4.141366 0.000000 9.250000 c
+0.000000 14.358634 4.141366 18.500000 9.250000 18.500000 c
+11.423139 18.500000 13.421247 17.750608 15.000000 16.496151 c
+15.000000 17.000000 l
+15.000000 17.552284 15.447716 18.000000 16.000000 18.000000 c
+16.552284 18.000000 17.000000 17.552284 17.000000 17.000000 c
+17.000000 14.301708 l
+17.011232 14.284512 17.022409 14.267276 17.033529 14.250000 c
+17.000000 14.250000 l
+17.000000 14.000000 l
+17.000000 13.447716 16.552284 13.000000 16.000000 13.000000 c
+13.000000 13.000000 l
+12.447715 13.000000 12.000000 13.447716 12.000000 14.000000 c
+12.000000 14.552284 12.447715 15.000000 13.000000 15.000000 c
+13.666476 15.000000 l
+12.443584 15.940684 10.912110 16.500000 9.250000 16.500000 c
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+ 1365
+endobj
+
+4 0 obj
+ << /Annots []
+ /Type /Page
+ /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+ /Resources 1 0 R
+ /Contents 2 0 R
+ /Parent 5 0 R
+ >>
+endobj
+
+5 0 obj
+ << /Kids [ 4 0 R ]
+ /Count 1
+ /Type /Pages
+ >>
+endobj
+
+6 0 obj
+ << /Pages 5 0 R
+ /Type /Catalog
+ >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001455 00000 n
+0000001478 00000 n
+0000001651 00000 n
+0000001725 00000 n
+trailer
+<< /ID [ (some) (id) ]
+ /Root 6 0 R
+ /Size 7
+>>
+startxref
+1784
+%%EOF
\ No newline at end of file
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json
new file mode 100644
index 000000000..92bff3aca
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/retry.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "Arrow Clockwise.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json
new file mode 100644
index 000000000..b2b588d4d
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "Dismiss.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf
new file mode 100644
index 000000000..0616f6275
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Compose/Attachment/stop.imageset/Dismiss.pdf
@@ -0,0 +1,89 @@
+%PDF-1.7
+
+1 0 obj
+ << >>
+endobj
+
+2 0 obj
+ << /Length 3 0 R >>
+stream
+/DeviceRGB CS
+/DeviceRGB cs
+q
+1.000000 0.000000 -0.000000 1.000000 4.000000 3.804749 cm
+0.000000 0.000000 0.000000 scn
+0.209704 15.808150 m
+0.292893 15.902358 l
+0.653377 16.262842 1.220608 16.290571 1.612899 15.985547 c
+1.707107 15.902358 l
+8.000000 9.610251 l
+14.292892 15.902358 l
+14.683416 16.292883 15.316584 16.292883 15.707108 15.902358 c
+16.097631 15.511834 16.097631 14.878669 15.707108 14.488145 c
+9.415000 8.195251 l
+15.707108 1.902359 l
+16.067591 1.541875 16.095320 0.974643 15.790295 0.582352 c
+15.707108 0.488144 l
+15.346623 0.127661 14.779391 0.099932 14.387100 0.404957 c
+14.292892 0.488144 l
+8.000000 6.780252 l
+1.707107 0.488144 l
+1.316582 0.097620 0.683418 0.097620 0.292893 0.488144 c
+-0.097631 0.878668 -0.097631 1.511835 0.292893 1.902359 c
+6.585000 8.195251 l
+0.292893 14.488145 l
+-0.067591 14.848629 -0.095320 15.415859 0.209704 15.808150 c
+0.292893 15.902358 l
+0.209704 15.808150 l
+h
+f
+n
+Q
+
+endstream
+endobj
+
+3 0 obj
+ 914
+endobj
+
+4 0 obj
+ << /Annots []
+ /Type /Page
+ /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
+ /Resources 1 0 R
+ /Contents 2 0 R
+ /Parent 5 0 R
+ >>
+endobj
+
+5 0 obj
+ << /Kids [ 4 0 R ]
+ /Count 1
+ /Type /Pages
+ >>
+endobj
+
+6 0 obj
+ << /Pages 5 0 R
+ /Type /Catalog
+ >>
+endobj
+
+xref
+0 7
+0000000000 65535 f
+0000000010 00000 n
+0000000034 00000 n
+0000001004 00000 n
+0000001026 00000 n
+0000001199 00000 n
+0000001273 00000 n
+trailer
+<< /ID [ (some) (id) ]
+ /Root 6 0 R
+ /Size 7
+>>
+startxref
+1332
+%%EOF
\ No newline at end of file
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json
new file mode 100644
index 000000000..6e965652d
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.background.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.background.colorset/Contents.json
new file mode 100644
index 000000000..86944ced3
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.background.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.852",
+ "green" : "0.894",
+ "red" : "0.835"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.354",
+ "green" : "0.353",
+ "red" : "0.268"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.checkmark.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.checkmark.colorset/Contents.json
new file mode 100644
index 000000000..f5112f04f
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.checkmark.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.371",
+ "green" : "0.565",
+ "red" : "0.290"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.603",
+ "green" : "0.742",
+ "red" : "0.476"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.link.colorset/Contents.json b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.link.colorset/Contents.json
new file mode 100644
index 000000000..f5112f04f
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonAsset/Assets.xcassets/Scene/Profile/About/bio.about.field.verified.link.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.371",
+ "green" : "0.565",
+ "red" : "0.290"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.603",
+ "green" : "0.742",
+ "red" : "0.476"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift
index 5cd0059d8..5db001d70 100644
--- a/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift
+++ b/MastodonSDK/Sources/MastodonAsset/Generated/Assets.swift
@@ -130,6 +130,11 @@ public enum Asset {
}
public enum Scene {
public enum Compose {
+ public enum Attachment {
+ public static let indicatorButtonBackground = ColorAsset(name: "Scene/Compose/Attachment/indicator.button.background")
+ public static let retry = ImageAsset(name: "Scene/Compose/Attachment/retry")
+ public static let stop = ImageAsset(name: "Scene/Compose/Attachment/stop")
+ }
public static let earth = ImageAsset(name: "Scene/Compose/Earth")
public static let mention = ImageAsset(name: "Scene/Compose/Mention")
public static let more = ImageAsset(name: "Scene/Compose/More")
@@ -163,6 +168,11 @@ public enum Asset {
public static let textFieldBackground = ColorAsset(name: "Scene/Onboarding/textField.background")
}
public enum Profile {
+ public enum About {
+ public static let bioAboutFieldVerifiedBackground = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.background")
+ public static let bioAboutFieldVerifiedCheckmark = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.checkmark")
+ public static let bioAboutFieldVerifiedLink = ColorAsset(name: "Scene/Profile/About/bio.about.field.verified.link")
+ }
public enum Banner {
public static let bioEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/bio.edit.background.gray")
public static let nameEditBackgroundGray = ColorAsset(name: "Scene/Profile/Banner/name.edit.background.gray")
diff --git a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift
index cebe7f8af..8d2f77ba7 100644
--- a/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift
+++ b/MastodonSDK/Sources/MastodonCore/Extension/CoreDataStack/MastodonUser+Property.swift
@@ -10,6 +10,10 @@ import CoreDataStack
import MastodonSDK
extension MastodonUser.Property {
+ public init(entity: Mastodon.Entity.Account, domain: String) {
+ self.init(entity: entity, domain: domain, networkDate: Date())
+ }
+
init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) {
self.init(
identifier: entity.id + "@" + domain,
diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift
index 52f522703..6174f4687 100644
--- a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift
+++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift
@@ -8,28 +8,28 @@
import Foundation
import MastodonSDK
-enum CustomEmojiPickerItem {
+public enum CustomEmojiPickerItem {
case emoji(attribute: CustomEmojiAttribute)
}
extension CustomEmojiPickerItem: Equatable, Hashable { }
extension CustomEmojiPickerItem {
- final class CustomEmojiAttribute: Equatable, Hashable {
- let id = UUID()
+ public final class CustomEmojiAttribute: Equatable, Hashable {
+ public let id = UUID()
- let emoji: Mastodon.Entity.Emoji
+ public let emoji: Mastodon.Entity.Emoji
- init(emoji: Mastodon.Entity.Emoji) {
+ public init(emoji: Mastodon.Entity.Emoji) {
self.emoji = emoji
}
- static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool {
+ public static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool {
return lhs.id == rhs.id &&
lhs.emoji.shortcode == rhs.emoji.shortcode
}
- func hash(into hasher: inout Hasher) {
+ public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift
index 68649d24c..1b6a57a83 100644
--- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift
+++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Account.swift
@@ -13,6 +13,15 @@ import MastodonCommon
import MastodonSDK
extension APIService {
+ public func authenticatedUserInfo(
+ authenticationBox: MastodonAuthenticationBox
+ ) async throws -> Mastodon.Response.Content {
+ try await accountInfo(
+ domain: authenticationBox.domain,
+ userID: authenticationBox.userID,
+ authorization: authenticationBox.userAuthorization
+ )
+ }
public func accountInfo(
domain: String,
diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift
index 48da254c6..0e5160679 100644
--- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift
+++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift
@@ -25,6 +25,7 @@ public final class AuthenticationService: NSObject {
// output
@Published public var mastodonAuthentications: [ManagedObjectRecord] = []
@Published public var mastodonAuthenticationBoxes: [MastodonAuthenticationBox] = []
+ public let updateActiveUserAccountPublisher = PassthroughSubject()
init(
managedObjectContext: NSManagedObjectContext,
diff --git a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift
index c63e965bd..4cd804036 100644
--- a/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift
+++ b/MastodonSDK/Sources/MastodonCore/Service/InstanceService.swift
@@ -24,7 +24,7 @@ public final class InstanceService {
weak var authenticationService: AuthenticationService?
// output
-
+
init(
apiService: APIService,
authenticationService: AuthenticationService
diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift
index 684ba439f..1f07ccc93 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift
+++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift
@@ -319,7 +319,7 @@ public enum L10n {
public static func email(_ p1: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Email", String(describing: p1))
}
- /// Hastag %@
+ /// Hashtag: %@
public static func hashtag(_ p1: Any) -> String {
return L10n.tr("Localizable", "Common.Controls.Status.MetaEntity.Hashtag", String(describing: p1))
}
@@ -449,6 +449,12 @@ public enum L10n {
public static let disableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.DisableContentWarning")
/// Enable Content Warning
public static let enableContentWarning = L10n.tr("Localizable", "Scene.Compose.Accessibility.EnableContentWarning")
+ /// Posting as %@
+ public static func postingAs(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "Scene.Compose.Accessibility.PostingAs", String(describing: p1))
+ }
+ /// Post Options
+ public static let postOptions = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostOptions")
/// Post Visibility Menu
public static let postVisibilityMenu = L10n.tr("Localizable", "Scene.Compose.Accessibility.PostVisibilityMenu")
/// Remove Poll
@@ -459,12 +465,24 @@ public enum L10n {
public static func attachmentBroken(_ p1: Any) -> String {
return L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentBroken", String(describing: p1))
}
+ /// Attachment too large
+ public static let attachmentTooLarge = L10n.tr("Localizable", "Scene.Compose.Attachment.AttachmentTooLarge")
+ /// Can not recognize this media attachment
+ public static let canNotRecognizeThisMediaAttachment = L10n.tr("Localizable", "Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment")
+ /// Compressing...
+ public static let compressingState = L10n.tr("Localizable", "Scene.Compose.Attachment.CompressingState")
/// Describe the photo for the visually-impaired...
public static let descriptionPhoto = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionPhoto")
/// Describe the video for the visually-impaired...
public static let descriptionVideo = L10n.tr("Localizable", "Scene.Compose.Attachment.DescriptionVideo")
+ /// Load Failed
+ public static let loadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.LoadFailed")
/// photo
public static let photo = L10n.tr("Localizable", "Scene.Compose.Attachment.Photo")
+ /// Server Processing...
+ public static let serverProcessingState = L10n.tr("Localizable", "Scene.Compose.Attachment.ServerProcessingState")
+ /// Upload Failed
+ public static let uploadFailed = L10n.tr("Localizable", "Scene.Compose.Attachment.UploadFailed")
/// video
public static let video = L10n.tr("Localizable", "Scene.Compose.Attachment.Video")
}
@@ -519,6 +537,10 @@ public enum L10n {
public static let sevenDays = L10n.tr("Localizable", "Scene.Compose.Poll.SevenDays")
/// 6 Hours
public static let sixHours = L10n.tr("Localizable", "Scene.Compose.Poll.SixHours")
+ /// The poll has empty option
+ public static let thePollHasEmptyOption = L10n.tr("Localizable", "Scene.Compose.Poll.ThePollHasEmptyOption")
+ /// The poll is invalid
+ public static let thePollIsInvalid = L10n.tr("Localizable", "Scene.Compose.Poll.ThePollIsInvalid")
/// 30 minutes
public static let thirtyMinutes = L10n.tr("Localizable", "Scene.Compose.Poll.ThirtyMinutes")
/// 3 Days
@@ -713,6 +735,16 @@ public enum L10n {
/// Label
public static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label")
}
+ public enum Verified {
+ /// Ownership of this link was checked on %@
+ public static func long(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Long", String(describing: p1))
+ }
+ /// Verified on %@
+ public static func short(_ p1: Any) -> String {
+ return L10n.tr("Localizable", "Scene.Profile.Fields.Verified.Short", String(describing: p1))
+ }
+ }
}
public enum Header {
/// Follows You
@@ -1276,6 +1308,10 @@ public enum L10n {
public enum A11y {
public enum Plural {
public enum Count {
+ /// Plural format key: "%#@character_count@ left"
+ public static func charactersLeft(_ p1: Int) -> String {
+ return L10n.tr("Localizable", "a11y.plural.count.characters_left", p1)
+ }
/// Plural format key: "Input limit exceeds %#@character_count@"
public static func inputLimitExceeds(_ p1: Int) -> String {
return L10n.tr("Localizable", "a11y.plural.count.input_limit_exceeds", p1)
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings
new file mode 100644
index 000000000..1831b3ba9
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings
@@ -0,0 +1,463 @@
+"Common.Alerts.BlockDomain.BlockEntireDomain" = "Block Domain";
+"Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain and any of your followers from that domain will be removed.";
+"Common.Alerts.CleanCache.Message" = "Successfully cleaned %@ cache.";
+"Common.Alerts.CleanCache.Title" = "Clean Cache";
+"Common.Alerts.Common.PleaseTryAgain" = "Please try again.";
+"Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later.";
+"Common.Alerts.DeletePost.Message" = "Are you sure you want to delete this post?";
+"Common.Alerts.DeletePost.Title" = "Delete Post";
+"Common.Alerts.DiscardPostContent.Message" = "Confirm to discard composed post content.";
+"Common.Alerts.DiscardPostContent.Title" = "Discard Draft";
+"Common.Alerts.EditProfileFailure.Message" = "Cannot edit profile. Please try again.";
+"Common.Alerts.EditProfileFailure.Title" = "Edit Profile Error";
+"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" = "Publish Failure";
+"Common.Alerts.SavePhotoFailure.Message" = "Please enable the photo library access permission to save the photo.";
+"Common.Alerts.SavePhotoFailure.Title" = "Save Photo Failure";
+"Common.Alerts.ServerError.Title" = "Server Error";
+"Common.Alerts.SignOut.Confirm" = "Sign Out";
+"Common.Alerts.SignOut.Message" = "Are you sure you want to sign out?";
+"Common.Alerts.SignOut.Title" = "Sign Out";
+"Common.Alerts.SignUpFailure.Title" = "Sign Up Failure";
+"Common.Alerts.VoteFailure.PollEnded" = "The poll has ended";
+"Common.Alerts.VoteFailure.Title" = "Vote Failure";
+"Common.Controls.Actions.Add" = "Add";
+"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";
+"Common.Controls.Actions.Delete" = "Delete";
+"Common.Controls.Actions.Discard" = "Discard";
+"Common.Controls.Actions.Done" = "Done";
+"Common.Controls.Actions.Edit" = "Edit";
+"Common.Controls.Actions.FindPeople" = "Find people to follow";
+"Common.Controls.Actions.ManuallySearch" = "Manually search instead";
+"Common.Controls.Actions.Next" = "Next";
+"Common.Controls.Actions.Ok" = "OK";
+"Common.Controls.Actions.Open" = "Open";
+"Common.Controls.Actions.OpenInBrowser" = "Open in Browser";
+"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
+"Common.Controls.Actions.Preview" = "Preview";
+"Common.Controls.Actions.Previous" = "Previous";
+"Common.Controls.Actions.Remove" = "Remove";
+"Common.Controls.Actions.Reply" = "Reply";
+"Common.Controls.Actions.ReportUser" = "Report %@";
+"Common.Controls.Actions.Save" = "Save";
+"Common.Controls.Actions.SavePhoto" = "Save Photo";
+"Common.Controls.Actions.SeeMore" = "See More";
+"Common.Controls.Actions.Settings" = "Settings";
+"Common.Controls.Actions.Share" = "Share";
+"Common.Controls.Actions.SharePost" = "Share Post";
+"Common.Controls.Actions.ShareUser" = "Share %@";
+"Common.Controls.Actions.SignIn" = "Sign In";
+"Common.Controls.Actions.SignUp" = "Sign Up";
+"Common.Controls.Actions.Skip" = "Skip";
+"Common.Controls.Actions.TakePhoto" = "Take Photo";
+"Common.Controls.Actions.TryAgain" = "Try Again";
+"Common.Controls.Actions.UnblockDomain" = "Unblock %@";
+"Common.Controls.Friendship.Block" = "Block";
+"Common.Controls.Friendship.BlockDomain" = "Block %@";
+"Common.Controls.Friendship.BlockUser" = "Block %@";
+"Common.Controls.Friendship.Blocked" = "Blocked";
+"Common.Controls.Friendship.EditInfo" = "Edit Info";
+"Common.Controls.Friendship.Follow" = "Follow";
+"Common.Controls.Friendship.Following" = "Following";
+"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
+"Common.Controls.Friendship.Mute" = "Mute";
+"Common.Controls.Friendship.MuteUser" = "Mute %@";
+"Common.Controls.Friendship.Muted" = "Muted";
+"Common.Controls.Friendship.Pending" = "Pending";
+"Common.Controls.Friendship.Request" = "Request";
+"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
+"Common.Controls.Friendship.Unblock" = "Unblock";
+"Common.Controls.Friendship.UnblockUser" = "Unblock %@";
+"Common.Controls.Friendship.Unmute" = "Unmute";
+"Common.Controls.Friendship.UnmuteUser" = "Unmute %@";
+"Common.Controls.Keyboard.Common.ComposeNewPost" = "Compose New Post";
+"Common.Controls.Keyboard.Common.OpenSettings" = "Open Settings";
+"Common.Controls.Keyboard.Common.ShowFavorites" = "Show Favorites";
+"Common.Controls.Keyboard.Common.SwitchToTab" = "Switch to %@";
+"Common.Controls.Keyboard.SegmentedControl.NextSection" = "Next Section";
+"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Previous Section";
+"Common.Controls.Keyboard.Timeline.NextStatus" = "Next Post";
+"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Open Author's Profile";
+"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Open Reblogger's Profile";
+"Common.Controls.Keyboard.Timeline.OpenStatus" = "Open Post";
+"Common.Controls.Keyboard.Timeline.PreviewImage" = "Preview Image";
+"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Previous Post";
+"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Reply to Post";
+"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.Status.Actions.Favorite" = "Favorite";
+"Common.Controls.Status.Actions.Hide" = "Hide";
+"Common.Controls.Status.Actions.Menu" = "Menu";
+"Common.Controls.Status.Actions.Reblog" = "Reblog";
+"Common.Controls.Status.Actions.Reply" = "Reply";
+"Common.Controls.Status.Actions.ShowGif" = "Show GIF";
+"Common.Controls.Status.Actions.ShowImage" = "Show image";
+"Common.Controls.Status.Actions.ShowVideoPlayer" = "Show video player";
+"Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Tap then hold to show menu";
+"Common.Controls.Status.Actions.Unfavorite" = "Unfavorite";
+"Common.Controls.Status.Actions.Unreblog" = "Undo reblog";
+"Common.Controls.Status.ContentWarning" = "Content Warning";
+"Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
+"Common.Controls.Status.Poll.Closed" = "Closed";
+"Common.Controls.Status.Poll.Vote" = "Vote";
+"Common.Controls.Status.SensitiveContent" = "Sensitive Content";
+"Common.Controls.Status.ShowPost" = "Show Post";
+"Common.Controls.Status.ShowUserProfile" = "Show user profile";
+"Common.Controls.Status.Tag.Email" = "Email";
+"Common.Controls.Status.Tag.Emoji" = "Emoji";
+"Common.Controls.Status.Tag.Hashtag" = "Hashtag";
+"Common.Controls.Status.Tag.Link" = "Link";
+"Common.Controls.Status.Tag.Mention" = "Mention";
+"Common.Controls.Status.Tag.Url" = "URL";
+"Common.Controls.Status.TapToReveal" = "Tap to reveal";
+"Common.Controls.Status.UserReblogged" = "%@ reblogged";
+"Common.Controls.Status.UserRepliedTo" = "Replied to %@";
+"Common.Controls.Status.Visibility.Direct" = "Only mentioned user can see this post.";
+"Common.Controls.Status.Visibility.Private" = "Only their followers can see this post.";
+"Common.Controls.Status.Visibility.PrivateFromMe" = "Only my followers can see this post.";
+"Common.Controls.Status.Visibility.Unlisted" = "Everyone can see this post but not display in the public timeline.";
+"Common.Controls.Tabs.Home" = "Home";
+"Common.Controls.Tabs.Notifications" = "Notifications";
+"Common.Controls.Tabs.Profile" = "Profile";
+"Common.Controls.Tabs.Search" = "Search";
+"Common.Controls.Timeline.Filtered" = "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.NoStatusFound" = "No Post Found";
+"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.Loader.LoadMissingPosts" = "Load missing posts";
+"Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts...";
+"Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies";
+"Common.Controls.Timeline.Timestamp.Now" = "Now";
+"Scene.AccountList.AddAccount" = "Add Account";
+"Scene.AccountList.DismissAccountSwitcher" = "Dismiss Account Switcher";
+"Scene.AccountList.TabBarHint" = "Current selected profile: %@. Double tap then hold to show account switcher";
+"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Compose.Accessibility.AppendAttachment" = "Add Attachment";
+"Scene.Compose.Accessibility.AppendPoll" = "Add Poll";
+"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker";
+"Scene.Compose.Accessibility.DisableContentWarning" = "Disable Content Warning";
+"Scene.Compose.Accessibility.EnableContentWarning" = "Enable Content Warning";
+"Scene.Compose.Accessibility.PostOptions" = "Post Options";
+"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post Visibility Menu";
+"Scene.Compose.Accessibility.PostingAs" = "Posting as %@";
+"Scene.Compose.Accessibility.RemovePoll" = "Remove Poll";
+"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
+uploaded to Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not recognize this media attachment";
+"Scene.Compose.Attachment.CompressingState" = "Compressing...";
+"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.LoadFailed" = "Load Failed";
+"Scene.Compose.Attachment.Photo" = "photo";
+"Scene.Compose.Attachment.ServerProcessingState" = "Server Processing...";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
+"Scene.Compose.Attachment.Video" = "video";
+"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
+"Scene.Compose.ComposeAction" = "Publish";
+"Scene.Compose.ContentInputPlaceholder" = "Type or paste what’s on your mind";
+"Scene.Compose.ContentWarning.Placeholder" = "Write an accurate warning here...";
+"Scene.Compose.Keyboard.AppendAttachmentEntry" = "Add Attachment - %@";
+"Scene.Compose.Keyboard.DiscardPost" = "Discard Post";
+"Scene.Compose.Keyboard.PublishPost" = "Publish Post";
+"Scene.Compose.Keyboard.SelectVisibilityEntry" = "Select Visibility - %@";
+"Scene.Compose.Keyboard.ToggleContentWarning" = "Toggle Content Warning";
+"Scene.Compose.Keyboard.TogglePoll" = "Toggle Poll";
+"Scene.Compose.MediaSelection.Browse" = "Browse";
+"Scene.Compose.MediaSelection.Camera" = "Take Photo";
+"Scene.Compose.MediaSelection.PhotoLibrary" = "Photo Library";
+"Scene.Compose.Poll.DurationTime" = "Duration: %@";
+"Scene.Compose.Poll.OneDay" = "1 Day";
+"Scene.Compose.Poll.OneHour" = "1 Hour";
+"Scene.Compose.Poll.OptionNumber" = "Option %ld";
+"Scene.Compose.Poll.SevenDays" = "7 Days";
+"Scene.Compose.Poll.SixHours" = "6 Hours";
+"Scene.Compose.Poll.ThePollHasEmptyOption" = "The poll has empty option";
+"Scene.Compose.Poll.ThePollIsInvalid" = "The poll is invalid";
+"Scene.Compose.Poll.ThirtyMinutes" = "30 minutes";
+"Scene.Compose.Poll.ThreeDays" = "3 Days";
+"Scene.Compose.ReplyingToUser" = "replying to %@";
+"Scene.Compose.Title.NewPost" = "New Post";
+"Scene.Compose.Title.NewReply" = "New Reply";
+"Scene.Compose.Visibility.Direct" = "Only people I mention";
+"Scene.Compose.Visibility.Private" = "Followers only";
+"Scene.Compose.Visibility.Public" = "Public";
+"Scene.Compose.Visibility.Unlisted" = "Unlisted";
+"Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App";
+"Scene.ConfirmEmail.Button.Resend" = "Resend";
+"Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";
+"Scene.ConfirmEmail.DontReceiveEmail.ResendEmail" = "Resend Email";
+"Scene.ConfirmEmail.DontReceiveEmail.Title" = "Check your email";
+"Scene.ConfirmEmail.OpenEmailApp.Description" = "We just sent you an email. Check your junk folder if you haven’t.";
+"Scene.ConfirmEmail.OpenEmailApp.Mail" = "Mail";
+"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Open Email Client";
+"Scene.ConfirmEmail.OpenEmailApp.Title" = "Check your inbox.";
+"Scene.ConfirmEmail.Subtitle" = "Tap the link we emailed to you to verify your account.";
+"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account";
+"Scene.ConfirmEmail.Title" = "One last thing.";
+"Scene.Discovery.Intro" = "These are the posts gaining traction in your corner of Mastodon.";
+"Scene.Discovery.Tabs.Community" = "Community";
+"Scene.Discovery.Tabs.ForYou" = "For You";
+"Scene.Discovery.Tabs.Hashtags" = "Hashtags";
+"Scene.Discovery.Tabs.News" = "News";
+"Scene.Discovery.Tabs.Posts" = "Posts";
+"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@";
+"Scene.Familiarfollowers.Title" = "Followers you familiar";
+"Scene.Favorite.Title" = "Your Favorites";
+"Scene.FavoritedBy.Title" = "Favorited By";
+"Scene.Follower.Footer" = "Followers from other servers are not displayed.";
+"Scene.Follower.Title" = "follower";
+"Scene.Following.Footer" = "Follows from other servers are not displayed.";
+"Scene.Following.Title" = "following";
+"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tap to scroll to top and tap again to previous location";
+"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Logo Button";
+"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
+"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
+"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
+"Scene.HomeTimeline.NavigationBarState.Publishing" = "Publishing post...";
+"Scene.HomeTimeline.Title" = "Home";
+"Scene.Notification.FollowRequest.Accept" = "Accept";
+"Scene.Notification.FollowRequest.Accepted" = "Accepted";
+"Scene.Notification.FollowRequest.Reject" = "reject";
+"Scene.Notification.FollowRequest.Rejected" = "Rejected";
+"Scene.Notification.Keyobard.ShowEverything" = "Show Everything";
+"Scene.Notification.Keyobard.ShowMentions" = "Show Mentions";
+"Scene.Notification.NotificationDescription.FavoritedYourPost" = "favorited your post";
+"Scene.Notification.NotificationDescription.FollowedYou" = "followed you";
+"Scene.Notification.NotificationDescription.MentionedYou" = "mentioned you";
+"Scene.Notification.NotificationDescription.PollHasEnded" = "poll has ended";
+"Scene.Notification.NotificationDescription.RebloggedYourPost" = "reblogged your post";
+"Scene.Notification.NotificationDescription.RequestToFollowYou" = "request to follow you";
+"Scene.Notification.Title.Everything" = "Everything";
+"Scene.Notification.Title.Mentions" = "Mentions";
+"Scene.Preview.Keyboard.ClosePreview" = "Close Preview";
+"Scene.Preview.Keyboard.ShowNext" = "Show Next";
+"Scene.Preview.Keyboard.ShowPrevious" = "Show Previous";
+"Scene.Profile.Accessibility.DoubleTapToOpenTheList" = "Double tap to open the list";
+"Scene.Profile.Accessibility.EditAvatarImage" = "Edit avatar image";
+"Scene.Profile.Accessibility.ShowAvatarImage" = "Show avatar image";
+"Scene.Profile.Accessibility.ShowBannerImage" = "Show banner image";
+"Scene.Profile.Dashboard.Followers" = "followers";
+"Scene.Profile.Dashboard.Following" = "following";
+"Scene.Profile.Dashboard.Posts" = "posts";
+"Scene.Profile.Fields.AddRow" = "Add Row";
+"Scene.Profile.Fields.Placeholder.Content" = "Content";
+"Scene.Profile.Fields.Placeholder.Label" = "Label";
+"Scene.Profile.Fields.Verified.Long" = "Ownership of this link was checked on %@";
+"Scene.Profile.Fields.Verified.Short" = "Verified on %@";
+"Scene.Profile.Header.FollowsYou" = "Follows You";
+"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirm to block %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Block Account";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirm to mute %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Mute Account";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirm to unblock %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Unblock Account";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm to unmute %@";
+"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
+"Scene.Profile.SegmentedControl.About" = "About";
+"Scene.Profile.SegmentedControl.Media" = "Media";
+"Scene.Profile.SegmentedControl.Posts" = "Posts";
+"Scene.Profile.SegmentedControl.PostsAndReplies" = "Posts and Replies";
+"Scene.Profile.SegmentedControl.Replies" = "Replies";
+"Scene.RebloggedBy.Title" = "Reblogged By";
+"Scene.Register.Error.Item.Agreement" = "Agreement";
+"Scene.Register.Error.Item.Email" = "Email";
+"Scene.Register.Error.Item.Locale" = "Locale";
+"Scene.Register.Error.Item.Password" = "Password";
+"Scene.Register.Error.Item.Reason" = "Reason";
+"Scene.Register.Error.Item.Username" = "Username";
+"Scene.Register.Error.Reason.Accepted" = "%@ must be accepted";
+"Scene.Register.Error.Reason.Blank" = "%@ is required";
+"Scene.Register.Error.Reason.Blocked" = "%@ contains a disallowed email provider";
+"Scene.Register.Error.Reason.Inclusion" = "%@ is not a supported value";
+"Scene.Register.Error.Reason.Invalid" = "%@ is invalid";
+"Scene.Register.Error.Reason.Reserved" = "%@ is a reserved keyword";
+"Scene.Register.Error.Reason.Taken" = "%@ is already in use";
+"Scene.Register.Error.Reason.TooLong" = "%@ is too long";
+"Scene.Register.Error.Reason.TooShort" = "%@ is too short";
+"Scene.Register.Error.Reason.Unreachable" = "%@ does not seem to exist";
+"Scene.Register.Error.Special.EmailInvalid" = "This is not a valid email address";
+"Scene.Register.Error.Special.PasswordTooShort" = "Password is too short (must be at least 8 characters)";
+"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.Input.Avatar.Delete" = "Delete";
+"Scene.Register.Input.DisplayName.Placeholder" = "display name";
+"Scene.Register.Input.Email.Placeholder" = "email";
+"Scene.Register.Input.Invite.RegistrationUserInviteRequest" = "Why do you want to join?";
+"Scene.Register.Input.Password.Accessibility.Checked" = "checked";
+"Scene.Register.Input.Password.Accessibility.Unchecked" = "unchecked";
+"Scene.Register.Input.Password.CharacterLimit" = "8 characters";
+"Scene.Register.Input.Password.Hint" = "Your password needs at least eight characters";
+"Scene.Register.Input.Password.Placeholder" = "password";
+"Scene.Register.Input.Password.Require" = "Your password needs at least:";
+"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
+"Scene.Register.Input.Username.Placeholder" = "username";
+"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@";
+"Scene.Register.Title" = "Let’s get you set up on %@";
+"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.ReportSentTitle" = "Thanks for reporting, we’ll look into this.";
+"Scene.Report.Reported" = "REPORTED";
+"Scene.Report.Send" = "Send Report";
+"Scene.Report.SkipToSend" = "Send without comment";
+"Scene.Report.Step1" = "Step 1 of 2";
+"Scene.Report.Step2" = "Step 2 of 2";
+"Scene.Report.StepFinal.BlockUser" = "Block %@";
+"Scene.Report.StepFinal.DontWantToSeeThis" = "Don’t want to see this?";
+"Scene.Report.StepFinal.MuteUser" = "Mute %@";
+"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.";
+"Scene.Report.StepFinal.Unfollow" = "Unfollow";
+"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@";
+"Scene.Report.StepFinal.Unfollowed" = "Unfollowed";
+"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you don’t like on Mastodon, you can remove the person from your experience.";
+"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@";
+"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.";
+"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?";
+"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4";
+"Scene.Report.StepOne.IDontLikeIt" = "I don’t like it";
+"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see";
+"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules";
+"Scene.Report.StepOne.ItsSomethingElse" = "It’s something else";
+"Scene.Report.StepOne.ItsSpam" = "It’s spam";
+"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies";
+"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match";
+"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4";
+"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories";
+"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?";
+"Scene.Report.StepOne.WhatsWrongWithThisPost" = "What's wrong with this post?";
+"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?";
+"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules";
+"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?";
+"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply";
+"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4";
+"Scene.Report.StepTwo.IJustDon’tLikeIt" = "I just don’t like it";
+"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply";
+"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4";
+"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?";
+"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
+"Scene.Report.Title" = "Report %@";
+"Scene.Report.TitleReport" = "Report";
+"Scene.Search.Recommend.Accounts.Description" = "You may like to follow these accounts";
+"Scene.Search.Recommend.Accounts.Follow" = "Follow";
+"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
+"Scene.Search.Recommend.ButtonText" = "See All";
+"Scene.Search.Recommend.HashTag.Description" = "Hashtags that are getting quite a bit of attention";
+"Scene.Search.Recommend.HashTag.PeopleTalking" = "%@ people are talking";
+"Scene.Search.Recommend.HashTag.Title" = "Trending on Mastodon";
+"Scene.Search.SearchBar.Cancel" = "Cancel";
+"Scene.Search.SearchBar.Placeholder" = "Search hashtags and users";
+"Scene.Search.Searching.Clear" = "Clear";
+"Scene.Search.Searching.EmptyState.NoResults" = "No results";
+"Scene.Search.Searching.RecentSearch" = "Recent searches";
+"Scene.Search.Searching.Segment.All" = "All";
+"Scene.Search.Searching.Segment.Hashtags" = "Hashtags";
+"Scene.Search.Searching.Segment.People" = "People";
+"Scene.Search.Searching.Segment.Posts" = "Posts";
+"Scene.Search.Title" = "Search";
+"Scene.ServerPicker.Button.Category.Academia" = "academia";
+"Scene.ServerPicker.Button.Category.Activism" = "activism";
+"Scene.ServerPicker.Button.Category.All" = "All";
+"Scene.ServerPicker.Button.Category.AllAccessiblityDescription" = "Category: All";
+"Scene.ServerPicker.Button.Category.Art" = "art";
+"Scene.ServerPicker.Button.Category.Food" = "food";
+"Scene.ServerPicker.Button.Category.Furry" = "furry";
+"Scene.ServerPicker.Button.Category.Games" = "games";
+"Scene.ServerPicker.Button.Category.General" = "general";
+"Scene.ServerPicker.Button.Category.Journalism" = "journalism";
+"Scene.ServerPicker.Button.Category.Lgbt" = "lgbt";
+"Scene.ServerPicker.Button.Category.Music" = "music";
+"Scene.ServerPicker.Button.Category.Regional" = "regional";
+"Scene.ServerPicker.Button.Category.Tech" = "tech";
+"Scene.ServerPicker.Button.SeeLess" = "See Less";
+"Scene.ServerPicker.Button.SeeMore" = "See More";
+"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading the data. Check your internet connection.";
+"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
+"Scene.ServerPicker.EmptyState.NoResults" = "No results";
+"Scene.ServerPicker.Input.Placeholder" = "Search servers";
+"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL";
+"Scene.ServerPicker.Label.Category" = "CATEGORY";
+"Scene.ServerPicker.Label.Language" = "LANGUAGE";
+"Scene.ServerPicker.Label.Users" = "USERS";
+"Scene.ServerPicker.Subtitle" = "Pick a server based on your interests, region, or a general purpose one.";
+"Scene.ServerPicker.SubtitleExtend" = "Pick a server based on your interests, region, or a general purpose one. Each server is operated by an entirely independent organization or individual.";
+"Scene.ServerPicker.Title" = "Mastodon is made of users in different servers.";
+"Scene.ServerRules.Button.Confirm" = "I Agree";
+"Scene.ServerRules.PrivacyPolicy" = "privacy policy";
+"Scene.ServerRules.Prompt" = "By continuing, you’re subject to the terms of service and privacy policy for %@.";
+"Scene.ServerRules.Subtitle" = "These are set and enforced by the %@ moderators.";
+"Scene.ServerRules.TermsOfService" = "terms of service";
+"Scene.ServerRules.Title" = "Some ground rules.";
+"Scene.Settings.Footer.MastodonDescription" = "Mastodon is open source software. You can report issues on GitHub at %@ (%@)";
+"Scene.Settings.Keyboard.CloseSettingsWindow" = "Close Settings Window";
+"Scene.Settings.Section.Appearance.Automatic" = "Automatic";
+"Scene.Settings.Section.Appearance.Dark" = "Always Dark";
+"Scene.Settings.Section.Appearance.Light" = "Always Light";
+"Scene.Settings.Section.Appearance.Title" = "Appearance";
+"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.LookAndFeel.Light" = "Light";
+"Scene.Settings.Section.LookAndFeel.ReallyDark" = "Really Dark";
+"Scene.Settings.Section.LookAndFeel.SortaDark" = "Sorta Dark";
+"Scene.Settings.Section.LookAndFeel.Title" = "Look and Feel";
+"Scene.Settings.Section.LookAndFeel.UseSystem" = "Use System";
+"Scene.Settings.Section.Notifications.Boosts" = "Reblogs my post";
+"Scene.Settings.Section.Notifications.Favorites" = "Favorites my post";
+"Scene.Settings.Section.Notifications.Follows" = "Follows me";
+"Scene.Settings.Section.Notifications.Mentions" = "Mentions me";
+"Scene.Settings.Section.Notifications.Title" = "Notifications";
+"Scene.Settings.Section.Notifications.Trigger.Anyone" = "anyone";
+"Scene.Settings.Section.Notifications.Trigger.Follow" = "anyone I follow";
+"Scene.Settings.Section.Notifications.Trigger.Follower" = "a follower";
+"Scene.Settings.Section.Notifications.Trigger.Noone" = "no one";
+"Scene.Settings.Section.Notifications.Trigger.Title" = "Notify me when";
+"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Disable animated avatars";
+"Scene.Settings.Section.Preference.DisableEmojiAnimation" = "Disable animated emojis";
+"Scene.Settings.Section.Preference.OpenLinksInMastodon" = "Open links in Mastodon";
+"Scene.Settings.Section.Preference.Title" = "Preferences";
+"Scene.Settings.Section.Preference.TrueBlackDarkMode" = "True black dark mode";
+"Scene.Settings.Section.Preference.UsingDefaultBrowser" = "Use default browser to open links";
+"Scene.Settings.Section.SpicyZone.Clear" = "Clear Media Cache";
+"Scene.Settings.Section.SpicyZone.Signout" = "Sign Out";
+"Scene.Settings.Section.SpicyZone.Title" = "The Spicy Zone";
+"Scene.Settings.Title" = "Settings";
+"Scene.SuggestionAccount.FollowExplain" = "When you follow someone, you’ll see their posts in your home feed.";
+"Scene.SuggestionAccount.Title" = "Find People to Follow";
+"Scene.Thread.BackTitle" = "Post";
+"Scene.Thread.Title" = "Post from %@";
+"Scene.Welcome.GetStarted" = "Get Started";
+"Scene.Welcome.LogIn" = "Log In";
+"Scene.Welcome.Slogan" = "Social networking
+back in your hands.";
+"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
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict
new file mode 100644
index 000000000..cd97825f4
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict
@@ -0,0 +1,631 @@
+
+
+
+
+ a11y.plural.count.unread.notification
+
+ NSStringLocalizedFormatKey
+ %#@notification_count_unread_notification@
+ notification_count_unread_notification
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no unread notification
+ one
+ 1 unread notification
+ few
+ %ld unread notifications
+ many
+ %ld unread notification
+ other
+ %ld unread notification
+
+
+ a11y.plural.count.input_limit_exceeds
+
+ NSStringLocalizedFormatKey
+ Input limit exceeds %#@character_count@
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ a11y.plural.count.input_limit_remains
+
+ NSStringLocalizedFormatKey
+ Input limit remains %#@character_count@
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ a11y.plural.count.characters_left
+
+ NSStringLocalizedFormatKey
+ %#@character_count@ left
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
+ plural.count.followed_by_and_mutual
+
+ NSStringLocalizedFormatKey
+ %#@names@%#@count_mutual@
+ names
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ other
+
+
+ count_mutual
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ Followed by %1$@
+ one
+ Followed by %1$@, and another mutual
+ few
+ Followed by %1$@, and %ld mutuals
+ many
+ Followed by %1$@, and %ld mutuals
+ other
+ Followed by %1$@, and %ld mutuals
+
+
+ plural.count.metric_formatted.post
+
+ NSStringLocalizedFormatKey
+ %@ %#@post_count@
+ post_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ posts
+ one
+ post
+ few
+ posts
+ many
+ posts
+ other
+ posts
+
+
+ plural.count.media
+
+ NSStringLocalizedFormatKey
+ %#@media_count@
+ media_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 media
+ one
+ 1 media
+ few
+ %ld media
+ many
+ %ld media
+ other
+ %ld media
+
+
+ plural.count.post
+
+ NSStringLocalizedFormatKey
+ %#@post_count@
+ post_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 posts
+ one
+ 1 post
+ few
+ %ld posts
+ many
+ %ld posts
+ other
+ %ld posts
+
+
+ plural.count.favorite
+
+ NSStringLocalizedFormatKey
+ %#@favorite_count@
+ favorite_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 favorites
+ one
+ 1 favorite
+ few
+ %ld favorites
+ many
+ %ld favorites
+ other
+ %ld favorites
+
+
+ plural.count.reblog
+
+ NSStringLocalizedFormatKey
+ %#@reblog_count@
+ reblog_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 reblogs
+ one
+ 1 reblog
+ few
+ %ld reblogs
+ many
+ %ld reblogs
+ other
+ %ld reblogs
+
+
+ plural.count.reply
+
+ NSStringLocalizedFormatKey
+ %#@reply_count@
+ reply_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 replies
+ one
+ 1 reply
+ few
+ %ld replies
+ many
+ %ld replies
+ other
+ %ld replies
+
+
+ plural.count.vote
+
+ NSStringLocalizedFormatKey
+ %#@vote_count@
+ vote_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 votes
+ one
+ 1 vote
+ few
+ %ld votes
+ many
+ %ld votes
+ other
+ %ld votes
+
+
+ plural.count.voter
+
+ NSStringLocalizedFormatKey
+ %#@voter_count@
+ voter_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 voters
+ one
+ 1 voter
+ few
+ %ld voters
+ many
+ %ld voters
+ other
+ %ld voters
+
+
+ plural.people_talking
+
+ NSStringLocalizedFormatKey
+ %#@count_people_talking@
+ count_people_talking
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 people talking
+ one
+ 1 people talking
+ few
+ %ld people talking
+ many
+ %ld people talking
+ other
+ %ld people talking
+
+
+ plural.count.following
+
+ NSStringLocalizedFormatKey
+ %#@count_following@
+ count_following
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 following
+ one
+ 1 following
+ few
+ %ld following
+ many
+ %ld following
+ other
+ %ld following
+
+
+ plural.count.follower
+
+ NSStringLocalizedFormatKey
+ %#@count_follower@
+ count_follower
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 followers
+ one
+ 1 follower
+ few
+ %ld followers
+ many
+ %ld followers
+ other
+ %ld followers
+
+
+ date.year.left
+
+ NSStringLocalizedFormatKey
+ %#@count_year_left@
+ count_year_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 years left
+ one
+ 1 year left
+ few
+ %ld years left
+ many
+ %ld years left
+ other
+ %ld years left
+
+
+ date.month.left
+
+ NSStringLocalizedFormatKey
+ %#@count_month_left@
+ count_month_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 months left
+ one
+ 1 months left
+ few
+ %ld months left
+ many
+ %ld months left
+ other
+ %ld months left
+
+
+ date.day.left
+
+ NSStringLocalizedFormatKey
+ %#@count_day_left@
+ count_day_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 days left
+ one
+ 1 day left
+ few
+ %ld days left
+ many
+ %ld days left
+ other
+ %ld days left
+
+
+ date.hour.left
+
+ NSStringLocalizedFormatKey
+ %#@count_hour_left@
+ count_hour_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 hours left
+ one
+ 1 hour left
+ few
+ %ld hours left
+ many
+ %ld hours left
+ other
+ %ld hours left
+
+
+ date.minute.left
+
+ NSStringLocalizedFormatKey
+ %#@count_minute_left@
+ count_minute_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 minutes left
+ one
+ 1 minute left
+ few
+ %ld minutes left
+ many
+ %ld minutes left
+ other
+ %ld minutes left
+
+
+ date.second.left
+
+ NSStringLocalizedFormatKey
+ %#@count_second_left@
+ count_second_left
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0 seconds left
+ one
+ 1 second left
+ few
+ %ld seconds left
+ many
+ %ld seconds left
+ other
+ %ld seconds left
+
+
+ date.year.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_year_ago_abbr@
+ count_year_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0y ago
+ one
+ 1y ago
+ few
+ %ldy ago
+ many
+ %ldy ago
+ other
+ %ldy ago
+
+
+ date.month.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_month_ago_abbr@
+ count_month_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0M ago
+ one
+ 1M ago
+ few
+ %ldM ago
+ many
+ %ldM ago
+ other
+ %ldM ago
+
+
+ date.day.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_day_ago_abbr@
+ count_day_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0d ago
+ one
+ 1d ago
+ few
+ %ldd ago
+ many
+ %ldd ago
+ other
+ %ldd ago
+
+
+ date.hour.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_hour_ago_abbr@
+ count_hour_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0h ago
+ one
+ 1h ago
+ few
+ %ldh ago
+ many
+ %ldh ago
+ other
+ %ldh ago
+
+
+ date.minute.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_minute_ago_abbr@
+ count_minute_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0m ago
+ one
+ 1m ago
+ few
+ %ldm ago
+ many
+ %ldm ago
+ other
+ %ldm ago
+
+
+ date.second.ago.abbr
+
+ NSStringLocalizedFormatKey
+ %#@count_second_ago_abbr@
+ count_second_ago_abbr
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ 0s ago
+ one
+ 1s ago
+ few
+ %lds ago
+ many
+ %lds ago
+ other
+ %lds ago
+
+
+
+
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings
index 9ecfa450e..943d56a9f 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ar.lproj/Localizable.strings
@@ -68,13 +68,13 @@
"Common.Controls.Friendship.EditInfo" = "تَحريرُ المَعلُومات";
"Common.Controls.Friendship.Follow" = "مُتابَعَة";
"Common.Controls.Friendship.Following" = "مُتابَع";
-"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
+"Common.Controls.Friendship.HideReblogs" = "إخفاء إعادات التدوين";
"Common.Controls.Friendship.Mute" = "كَتم";
"Common.Controls.Friendship.MuteUser" = "كَتمُ %@";
"Common.Controls.Friendship.Muted" = "مكتوم";
"Common.Controls.Friendship.Pending" = "قيد المُراجعة";
"Common.Controls.Friendship.Request" = "إرسال طَلَب";
-"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
+"Common.Controls.Friendship.ShowReblogs" = "إظهار إعادات التدوين";
"Common.Controls.Friendship.Unblock" = "رفع الحَظر";
"Common.Controls.Friendship.UnblockUser" = "رفع الحَظر عن %@";
"Common.Controls.Friendship.Unmute" = "رفع الكتم";
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "التراجُع عن إعادة النشر";
"Common.Controls.Status.ContentWarning" = "تحذير المُحتوى";
"Common.Controls.Status.MediaContentWarning" = "اُنقُر لِلكَشف";
+"Common.Controls.Status.MetaEntity.Email" = "عُنوان البريد الإلكتُروني: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "وَسْم: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "إظهار المِلف التعريفي: %@";
+"Common.Controls.Status.MetaEntity.Url" = "رابِط: %@";
"Common.Controls.Status.Poll.Closed" = "انتهى";
"Common.Controls.Status.Poll.Vote" = "صَوِّت";
"Common.Controls.Status.SensitiveContent" = "مُحتَوى حَسَّاس";
@@ -151,7 +155,7 @@
"Scene.AccountList.AddAccount" = "إضافَةُ حِساب";
"Scene.AccountList.DismissAccountSwitcher" = "تجاهُل مبدِّل الحِساب";
"Scene.AccountList.TabBarHint" = "المِلَفُّ المُحدَّدُ حالِيًّا: %@. اُنقُر نَقرًا مُزدَوَجًا مَعَ الاِستِمرارِ لِإظهارِ مُبدِّلِ الحِساب";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "العَلاماتُ المَرجعيَّة";
"Scene.Compose.Accessibility.AppendAttachment" = "إضافة مُرفَق";
"Scene.Compose.Accessibility.AppendPoll" = "اضافة استطلاع رأي";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "منتقي الرموز التعبيرية المُخصَّص";
@@ -161,9 +165,13 @@
"Scene.Compose.Accessibility.RemovePoll" = "إزالة الاستطلاع";
"Scene.Compose.Attachment.AttachmentBroken" = "هذا ال%@ مُعطَّل
ويتعذَّرُ رفعُه إلى ماستودون.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "صِف الصورة للمَكفوفين...";
"Scene.Compose.Attachment.DescriptionVideo" = "صِف المقطع المرئي للمَكفوفين...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "صورة";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "مقطع مرئي";
"Scene.Compose.AutoComplete.SpaceToAdd" = "انقر على مساحة لإضافتِها";
"Scene.Compose.ComposeAction" = "نَشر";
@@ -256,12 +264,12 @@
"Scene.Profile.Header.FollowsYou" = "يُتابِعُك";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "تأكيدُ حَظر %@";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "حَظرُ الحِساب";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "التأكيد لِإخفاء إعادات التدوين";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "إخفاء إعادات التدوين";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "تأكيدُ كَتم %@";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "كَتمُ الحِساب";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "التأكيد لِإظهار إعادات التدوين";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "إظهار إعادات التدوين";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "تأكيدُ رَفع الحَظرِ عَن %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "رَفعُ الحَظرِ عَنِ الحِساب";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "أكِّد لرفع الكتمْ عن %@";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings
index 1e691f8a9..fca658aef 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ca.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Comprova la teva connexió a Internet.";
"Common.Controls.Status.Actions.Unreblog" = "Desfer l'impuls";
"Common.Controls.Status.ContentWarning" = "Advertència de Contingut";
"Common.Controls.Status.MediaContentWarning" = "Toca qualsevol lloc per a mostrar";
+"Common.Controls.Status.MetaEntity.Email" = "Correu electrònic: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Etiqueta %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Mostra el Perfil: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Enllaç: %@";
"Common.Controls.Status.Poll.Closed" = "Finalitzada";
"Common.Controls.Status.Poll.Vote" = "Vota";
"Common.Controls.Status.SensitiveContent" = "Contingut sensible";
@@ -151,7 +155,7 @@ El teu perfil els sembla així.";
"Scene.AccountList.AddAccount" = "Afegir compte";
"Scene.AccountList.DismissAccountSwitcher" = "Descartar el commutador de comptes";
"Scene.AccountList.TabBarHint" = "Perfil actual seleccionat: %@. Toca dues vegades i manté el dit per a mostrar el commutador de comptes";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Marcadors";
"Scene.Compose.Accessibility.AppendAttachment" = "Afegeix Adjunt";
"Scene.Compose.Accessibility.AppendPoll" = "Afegir enquesta";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector d'Emoji Personalitzat";
@@ -161,9 +165,13 @@ El teu perfil els sembla així.";
"Scene.Compose.Accessibility.RemovePoll" = "Eliminar Enquesta";
"Scene.Compose.Attachment.AttachmentBroken" = "Aquest %@ està trencat i no pot ser
carregat a Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "El fitxer adjunt és massa gran";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "No es pot reconèixer l'adjunt multimèdia";
"Scene.Compose.Attachment.DescriptionPhoto" = "Descriu la foto per als disminuïts visuals...";
"Scene.Compose.Attachment.DescriptionVideo" = "Descriu el vídeo per als disminuïts visuals...";
+"Scene.Compose.Attachment.LoadFailed" = "Ha fallat la càrrega";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Pujada fallida";
"Scene.Compose.Attachment.Video" = "vídeo";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Espai per afegir";
"Scene.Compose.ComposeAction" = "Publica";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings
index 053211f28..05c575520 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ckb.lproj/Localizable.strings
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "پۆستکردنەکە بگەڕێنەوە";
"Common.Controls.Status.ContentWarning" = "ئاگاداریی ناوەڕۆک";
"Common.Controls.Status.MediaContentWarning" = "دەستی پیا بنێ بۆ نیشاندانی";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "داخراوە";
"Common.Controls.Status.Poll.Vote" = "دەنگ بدە";
"Common.Controls.Status.SensitiveContent" = "ناوەڕۆکی هەستیار";
@@ -160,9 +164,13 @@
"Scene.Compose.Accessibility.PostVisibilityMenu" = "پێڕستی شێوازی دەرکەوتنی پۆست";
"Scene.Compose.Accessibility.RemovePoll" = "دانگدانەکە لابە";
"Scene.Compose.Attachment.AttachmentBroken" = "ئەم %@ـە تێک چووە و ناتوانیت بەرزی بکەیتەوە.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "وێنەکەت بۆ نابیناکان باس بکە...";
"Scene.Compose.Attachment.DescriptionVideo" = "ڤیدیۆکەت بۆ نابیناکان باس بکە...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "وێنە";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "ڤیدیۆ";
"Scene.Compose.AutoComplete.SpaceToAdd" = "بۆشایی دابنێ بۆ زیادکردن";
"Scene.Compose.ComposeAction" = "بڵاوی بکەوە";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings
index 0a78adc48..4f9a15906 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Bitte überprüfe deine Internetverbindung.";
"Common.Controls.Status.Actions.Unreblog" = "Nicht mehr teilen";
"Common.Controls.Status.ContentWarning" = "Inhaltswarnung";
"Common.Controls.Status.MediaContentWarning" = "Tippe irgendwo zum Anzeigen";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Beendet";
"Common.Controls.Status.Poll.Vote" = "Abstimmen";
"Common.Controls.Status.SensitiveContent" = "NSFW-Inhalt";
@@ -151,7 +155,7 @@ Dein Profil sieht für diesen Benutzer auch so aus.";
"Scene.AccountList.AddAccount" = "Konto hinzufügen";
"Scene.AccountList.DismissAccountSwitcher" = "Dialog zum Wechseln des Kontos schließen";
"Scene.AccountList.TabBarHint" = "Aktuell ausgewähltes Profil: %@. Doppeltippen dann gedrückt halten, um den Kontoschalter anzuzeigen";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Lesezeichen";
"Scene.Compose.Accessibility.AppendAttachment" = "Anhang hinzufügen";
"Scene.Compose.Accessibility.AppendPoll" = "Umfrage hinzufügen";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Benutzerdefinierter Emojiwähler";
@@ -161,9 +165,13 @@ Dein Profil sieht für diesen Benutzer auch so aus.";
"Scene.Compose.Accessibility.RemovePoll" = "Umfrage entfernen";
"Scene.Compose.Attachment.AttachmentBroken" = "Dieses %@ scheint defekt zu sein und
kann nicht auf Mastodon hochgeladen werden.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Anhang zu groß";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Medienanhang wurde nicht erkannt";
"Scene.Compose.Attachment.DescriptionPhoto" = "Für Menschen mit Sehbehinderung beschreiben...";
"Scene.Compose.Attachment.DescriptionVideo" = "Für Menschen mit Sehbehinderung beschreiben...";
+"Scene.Compose.Attachment.LoadFailed" = "Laden fehlgeschlagen";
"Scene.Compose.Attachment.Photo" = "Foto";
+"Scene.Compose.Attachment.UploadFailed" = "Upload fehlgeschlagen";
"Scene.Compose.Attachment.Video" = "Video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Leerzeichen um hinzuzufügen";
"Scene.Compose.ComposeAction" = "Veröffentlichen";
@@ -215,9 +223,9 @@ kann nicht auf Mastodon hochgeladen werden.";
"Scene.Familiarfollowers.Title" = "Follower, die dir bekannt vorkommen";
"Scene.Favorite.Title" = "Deine Favoriten";
"Scene.FavoritedBy.Title" = "Favorisiert von";
-"Scene.Follower.Footer" = "Follower von anderen Servern werden nicht angezeigt.";
+"Scene.Follower.Footer" = "Folger, die nicht auf deinem Server registriert sind, werden nicht angezeigt.";
"Scene.Follower.Title" = "Follower";
-"Scene.Following.Footer" = "Wem das Konto folgt wird von anderen Servern werden nicht angezeigt.";
+"Scene.Following.Footer" = "Gefolgte, die nicht auf deinem Server registriert sind, werden nicht angezeigt.";
"Scene.Following.Title" = "Folgende";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Zum Scrollen nach oben tippen und zum vorherigen Ort erneut tippen";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Logo-Button";
@@ -247,7 +255,7 @@ kann nicht auf Mastodon hochgeladen werden.";
"Scene.Profile.Accessibility.EditAvatarImage" = "Profilbild bearbeiten";
"Scene.Profile.Accessibility.ShowAvatarImage" = "Profilbild anzeigen";
"Scene.Profile.Accessibility.ShowBannerImage" = "Bannerbild anzeigen";
-"Scene.Profile.Dashboard.Followers" = "Folger";
+"Scene.Profile.Dashboard.Followers" = "Folgende";
"Scene.Profile.Dashboard.Following" = "Gefolgte";
"Scene.Profile.Dashboard.Posts" = "Beiträge";
"Scene.Profile.Fields.AddRow" = "Zeile hinzufügen";
@@ -260,7 +268,7 @@ kann nicht auf Mastodon hochgeladen werden.";
"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Reblogs ausblenden";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Bestätige %@ stumm zu schalten";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Konto stummschalten";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bestätigen um Reblogs anzuzeigen";
"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Reblogs anzeigen";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bestätige %@ zu entsperren";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Konto entsperren";
@@ -422,7 +430,7 @@ beliebigen Server.";
"Scene.Settings.Section.Notifications.Title" = "Benachrichtigungen";
"Scene.Settings.Section.Notifications.Trigger.Anyone" = "jeder";
"Scene.Settings.Section.Notifications.Trigger.Follow" = "ein von mir Gefolgter";
-"Scene.Settings.Section.Notifications.Trigger.Follower" = "ein Folger";
+"Scene.Settings.Section.Notifications.Trigger.Follower" = "ein Folgender";
"Scene.Settings.Section.Notifications.Trigger.Noone" = "niemand";
"Scene.Settings.Section.Notifications.Trigger.Title" = "Benachrichtige mich, wenn";
"Scene.Settings.Section.Preference.DisableAvatarAnimation" = "Animierte Profilbilder deaktivieren";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict
index c6a8a4297..f60c6b0d7 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/de.lproj/Localizable.stringsdict
@@ -248,9 +248,9 @@
NSStringFormatValueTypeKey
ld
one
- 1 Follower
+ 1 Folgender
other
- %ld Follower
+ %ld Folgende
date.year.left
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings
index ac09ab1f4..75b935422 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings
@@ -109,7 +109,7 @@ Please check your internet connection.";
"Common.Controls.Status.ContentWarning" = "Content Warning";
"Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal";
"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
-"Common.Controls.Status.MetaEntity.Hashtag" = "Hastag %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Closed";
@@ -161,13 +161,19 @@ Your profile looks like this to them.";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Custom Emoji Picker";
"Scene.Compose.Accessibility.DisableContentWarning" = "Disable Content Warning";
"Scene.Compose.Accessibility.EnableContentWarning" = "Enable Content Warning";
+"Scene.Compose.Accessibility.PostOptions" = "Post Options";
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Post Visibility Menu";
+"Scene.Compose.Accessibility.PostingAs" = "Posting as %@";
"Scene.Compose.Accessibility.RemovePoll" = "Remove Poll";
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
uploaded to Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"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.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "photo";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
"Scene.Compose.ComposeAction" = "Publish";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict
index bdcae6ac9..297e6675a 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict
@@ -50,6 +50,28 @@
%ld characters
+ a11y.plural.count.characters_left
+
+ NSStringLocalizedFormatKey
+ %#@character_count@ left
+ character_count
+
+ NSStringFormatSpecTypeKey
+ NSStringPluralRuleType
+ NSStringFormatValueTypeKey
+ ld
+ zero
+ no characters
+ one
+ 1 character
+ few
+ %ld characters
+ many
+ %ld characters
+ other
+ %ld characters
+
+
plural.count.followed_by_and_mutual
NSStringLocalizedFormatKey
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings
index 47ed11bb9..c16bec6cf 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/es.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Por favor, revise su conexión a internet.";
"Common.Controls.Status.Actions.Unreblog" = "Deshacer reblogueo";
"Common.Controls.Status.ContentWarning" = "Advertencia de Contenido";
"Common.Controls.Status.MediaContentWarning" = "Pulsa en cualquier sitio para mostrar";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Cerrado";
"Common.Controls.Status.Poll.Vote" = "Vota";
"Common.Controls.Status.SensitiveContent" = "Contenido sensible";
@@ -161,9 +165,13 @@ Tu perfil se ve así para él.";
"Scene.Compose.Accessibility.RemovePoll" = "Eliminar Encuesta";
"Scene.Compose.Attachment.AttachmentBroken" = "Este %@ está roto y no puede
subirse a Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe la foto para los usuarios con dificultad visual...";
"Scene.Compose.Attachment.DescriptionVideo" = "Describe el vídeo para los usuarios con dificultad visual...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "vídeo";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Espacio para añadir";
"Scene.Compose.ComposeAction" = "Publicar";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings
index e2be3068d..aef7a7507 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/eu.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Egiaztatu Interneteko konexioa.";
"Common.Controls.Status.Actions.Unreblog" = "Desegin bultzada";
"Common.Controls.Status.ContentWarning" = "Edukiaren abisua";
"Common.Controls.Status.MediaContentWarning" = "Ukitu edonon bistaratzeko";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Itxita";
"Common.Controls.Status.Poll.Vote" = "Bozkatu";
"Common.Controls.Status.SensitiveContent" = "Sensitive Content";
@@ -161,9 +165,13 @@ Zure profilak itxura hau du berarentzat.";
"Scene.Compose.Accessibility.RemovePoll" = "Kendu inkesta";
"Scene.Compose.Attachment.AttachmentBroken" = "%@ hondatuta dago eta ezin da
Mastodonera igo.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Deskribatu argazkia ikusmen arazoak dituztenentzat...";
"Scene.Compose.Attachment.DescriptionVideo" = "Deskribatu bideoa ikusmen arazoak dituztenentzat...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "argazkia";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "bideoa";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Sakatu zuriunea gehitzeko";
"Scene.Compose.ComposeAction" = "Argitaratu";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings
index fbf48fa83..11259ace5 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fi.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Tarkista internet-yhteytesi.";
"Common.Controls.Status.Actions.Unreblog" = "Peru edelleen jako";
"Common.Controls.Status.ContentWarning" = "Sisältövaroitus";
"Common.Controls.Status.MediaContentWarning" = "Napauta mistä tahansa paljastaaksesi";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Suljettu";
"Common.Controls.Status.Poll.Vote" = "Vote";
"Common.Controls.Status.SensitiveContent" = "Sensitive Content";
@@ -161,9 +165,13 @@ Profiilisi näyttää tältä hänelle.";
"Scene.Compose.Accessibility.RemovePoll" = "Poista kysely";
"Scene.Compose.Attachment.AttachmentBroken" = "This %@ is broken and can’t be
uploaded to Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Kuvaile kuva näkövammaisille...";
"Scene.Compose.Attachment.DescriptionVideo" = "Kuvaile video näkövammaisille...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "kuva";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Space to add";
"Scene.Compose.ComposeAction" = "Julkaise";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
index 03efc3549..931219c21 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Veuillez vérifier votre accès à Internet.";
"Common.Controls.Status.Actions.Unreblog" = "Annuler le reblog";
"Common.Controls.Status.ContentWarning" = "Avertissement de contenu";
"Common.Controls.Status.MediaContentWarning" = "Tapotez n’importe où pour révéler la publication";
+"Common.Controls.Status.MetaEntity.Email" = "Adresse e-mail : %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag : %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Afficher le profile : %@";
+"Common.Controls.Status.MetaEntity.Url" = "Lien : %@";
"Common.Controls.Status.Poll.Closed" = "Fermé";
"Common.Controls.Status.Poll.Vote" = "Voter";
"Common.Controls.Status.SensitiveContent" = "Contenu sensible";
@@ -151,7 +155,7 @@ Votre profil ressemble à ça pour lui.";
"Scene.AccountList.AddAccount" = "Ajouter un compte";
"Scene.AccountList.DismissAccountSwitcher" = "Rejeter le commutateur de compte";
"Scene.AccountList.TabBarHint" = "Profil sélectionné actuel: %@. Double appui puis maintenez enfoncé pour afficher le changement de compte";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Favoris";
"Scene.Compose.Accessibility.AppendAttachment" = "Joindre un document";
"Scene.Compose.Accessibility.AppendPoll" = "Ajouter un Sondage";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Sélecteur d’émojis personnalisés";
@@ -161,9 +165,13 @@ Votre profil ressemble à ça pour lui.";
"Scene.Compose.Accessibility.RemovePoll" = "Retirer le sondage";
"Scene.Compose.Attachment.AttachmentBroken" = "Ce %@ est brisé et ne peut pas être
téléversé sur Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Décrire cette photo pour les personnes malvoyantes...";
"Scene.Compose.Attachment.DescriptionVideo" = "Décrire cette vidéo pour les personnes malvoyantes...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "photo";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "vidéo";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Espace à ajouter";
"Scene.Compose.ComposeAction" = "Publier";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings
index 2d1964d81..ce1764eac 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gd.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Thoir sùil air a’ cheangal agad ris an eadar-lìon.";
"Common.Controls.Status.Actions.Unreblog" = "Na brosnaich tuilleadh";
"Common.Controls.Status.ContentWarning" = "Rabhadh susbainte";
"Common.Controls.Status.MediaContentWarning" = "Thoir gnogag àite sam bith gus a nochdadh";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Dùinte";
"Common.Controls.Status.Poll.Vote" = "Cuir bhòt";
"Common.Controls.Status.SensitiveContent" = "Susbaint fhrionasach";
@@ -161,9 +165,13 @@ Seo an coltas a th’ air a’ phròifil agad dhaibh-san.";
"Scene.Compose.Accessibility.RemovePoll" = "Thoir air falbh an cunntas-bheachd";
"Scene.Compose.Attachment.AttachmentBroken" = "Seo %@ a tha briste is cha ghabh
a luchdadh suas gu Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Mìnich an dealbh dhan fheadhainn air a bheil cion-lèirsinne…";
"Scene.Compose.Attachment.DescriptionVideo" = "Mìnich a’ video dhan fheadhainn air a bheil cion-lèirsinne…";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "dealbh";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Brùth air Space gus a chur ris";
"Scene.Compose.ComposeAction" = "Foillsich";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
index c76089221..3087f33c5 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
@@ -68,13 +68,13 @@ Comproba a conexión a internet.";
"Common.Controls.Friendship.EditInfo" = "Editar info";
"Common.Controls.Friendship.Follow" = "Seguir";
"Common.Controls.Friendship.Following" = "Seguindo";
-"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
+"Common.Controls.Friendship.HideReblogs" = "Agochar Promocións";
"Common.Controls.Friendship.Mute" = "Acalar";
"Common.Controls.Friendship.MuteUser" = "Acalar a %@";
"Common.Controls.Friendship.Muted" = "Acalada";
"Common.Controls.Friendship.Pending" = "Pendente";
"Common.Controls.Friendship.Request" = "Solicitar";
-"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
+"Common.Controls.Friendship.ShowReblogs" = "Mostrar Promocións";
"Common.Controls.Friendship.Unblock" = "Desbloquear";
"Common.Controls.Friendship.UnblockUser" = "Desbloquear a %@";
"Common.Controls.Friendship.Unmute" = "Non Acalar";
@@ -108,6 +108,10 @@ Comproba a conexión a internet.";
"Common.Controls.Status.Actions.Unreblog" = "Retirar promoción";
"Common.Controls.Status.ContentWarning" = "Aviso sobre o contido";
"Common.Controls.Status.MediaContentWarning" = "Toca nalgures para mostrar";
+"Common.Controls.Status.MetaEntity.Email" = "Enderezo de email: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Cancelo: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Mostrar Perfil: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Ligazón: %@";
"Common.Controls.Status.Poll.Closed" = "Pechada";
"Common.Controls.Status.Poll.Vote" = "Votar";
"Common.Controls.Status.SensitiveContent" = "Contido sensible";
@@ -151,7 +155,7 @@ Así se ve o teu perfil.";
"Scene.AccountList.AddAccount" = "Engadir conta";
"Scene.AccountList.DismissAccountSwitcher" = "Desbotar intercambiador de contas";
"Scene.AccountList.TabBarHint" = "Perfil seleccionado: %@. Dobre toque e manter para mostrar o intercambiador de contas";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Marcadores";
"Scene.Compose.Accessibility.AppendAttachment" = "Engadir anexo";
"Scene.Compose.Accessibility.AppendPoll" = "Engadir enquisa";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Selector emoji personalizado";
@@ -161,9 +165,13 @@ Así se ve o teu perfil.";
"Scene.Compose.Accessibility.RemovePoll" = "Eliminar enquisa";
"Scene.Compose.Attachment.AttachmentBroken" = "Este %@ está estragado e non pode
ser subido a Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Adxunto demasiado grande";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Non se recoñece o tipo de multimedia";
"Scene.Compose.Attachment.DescriptionPhoto" = "Describe a foto para persoas con problemas visuais...";
"Scene.Compose.Attachment.DescriptionVideo" = "Describe o vídeo para persoas con problemas visuais...";
+"Scene.Compose.Attachment.LoadFailed" = "Fallou a carga";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Erro na subida";
"Scene.Compose.Attachment.Video" = "vídeo";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Barra de espazo engade";
"Scene.Compose.ComposeAction" = "Publicar";
@@ -256,12 +264,12 @@ ser subido a Mastodon.";
"Scene.Profile.Header.FollowsYou" = "Séguete";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Confirma o bloqueo de %@";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Bloquear Conta";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirma para agochar promocións";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Agochar Promocións";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Confirma Acalar a %@";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Acalar conta";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirma para ver promocións";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Mostrar Promocións";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Confirma o desbloqueo de %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Desbloquear Conta";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirma restablecer a %@";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
index c83cb7458..8f99028ed 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Per favore verifica la tua connessione internet.";
"Common.Controls.Status.Actions.Unreblog" = "Annulla condivisione";
"Common.Controls.Status.ContentWarning" = "Avviso sul contenuto";
"Common.Controls.Status.MediaContentWarning" = "Tocca ovunque per rivelare";
+"Common.Controls.Status.MetaEntity.Email" = "Indirizzo email: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Mostra il profilo: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Collegamento: %@";
"Common.Controls.Status.Poll.Closed" = "Chiuso";
"Common.Controls.Status.Poll.Vote" = "Vota";
"Common.Controls.Status.SensitiveContent" = "Contenuto sensibile";
@@ -151,7 +155,7 @@ Il tuo profilo sembra questo per loro.";
"Scene.AccountList.AddAccount" = "Aggiungi account";
"Scene.AccountList.DismissAccountSwitcher" = "Ignora il cambio account";
"Scene.AccountList.TabBarHint" = "Profilo corrente selezionato: %@. Doppio tocco e tieni premuto per mostrare il cambio account";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Segnalibri";
"Scene.Compose.Accessibility.AppendAttachment" = "Aggiungi allegato";
"Scene.Compose.Accessibility.AppendPoll" = "Aggiungi sondaggio";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Selettore Emoji personalizzato";
@@ -161,9 +165,13 @@ Il tuo profilo sembra questo per loro.";
"Scene.Compose.Accessibility.RemovePoll" = "Elimina sondaggio";
"Scene.Compose.Attachment.AttachmentBroken" = "Questo %@ è rotto e non può essere
caricato su Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Allegato troppo grande";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Impossibile riconoscere questo allegato multimediale";
"Scene.Compose.Attachment.DescriptionPhoto" = "Descrivi la foto per gli utenti ipovedenti...";
"Scene.Compose.Attachment.DescriptionVideo" = "Descrivi il filmato per gli utenti ipovedenti...";
+"Scene.Compose.Attachment.LoadFailed" = "Caricamento fallito";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Caricamento fallito";
"Scene.Compose.Attachment.Video" = "filmato";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Spazio da aggiungere";
"Scene.Compose.ComposeAction" = "Pubblica";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
index 080624f06..cad44f531 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "ブーストを戻す";
"Common.Controls.Status.ContentWarning" = "コンテンツ警告";
"Common.Controls.Status.MediaContentWarning" = "どこかをタップして表示";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "終了";
"Common.Controls.Status.Poll.Vote" = "投票";
"Common.Controls.Status.SensitiveContent" = "閲覧注意";
@@ -156,9 +160,13 @@
"Scene.Compose.Accessibility.PostVisibilityMenu" = "投稿の表示メニュー";
"Scene.Compose.Accessibility.RemovePoll" = "投票を消去";
"Scene.Compose.Attachment.AttachmentBroken" = "%@は壊れていてMastodonにアップロードできません。";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "閲覧が難しいユーザーへの画像説明";
"Scene.Compose.Attachment.DescriptionVideo" = "閲覧が難しいユーザーへの映像説明";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "写真";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "動画";
"Scene.Compose.AutoComplete.SpaceToAdd" = "スペースを追加";
"Scene.Compose.ComposeAction" = "投稿";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
index 1339af4cf..03108a25a 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Ma ulac aɣilif, senqed tuqqna-inek internet.";
"Common.Controls.Status.Actions.Unreblog" = "Sefsex allus n usuffeɣ";
"Common.Controls.Status.ContentWarning" = "Alɣu n ugbur";
"Common.Controls.Status.MediaContentWarning" = "Sit anida tebɣiḍ i wakken ad twaliḍ";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Ifukk";
"Common.Controls.Status.Poll.Vote" = "Dɣeṛ";
"Common.Controls.Status.SensitiveContent" = "Agbur amḥulfu";
@@ -161,9 +165,13 @@ Akka i as-d-yettban umaɣnu-inek.";
"Scene.Compose.Accessibility.RemovePoll" = "Kkes asenqed";
"Scene.Compose.Attachment.AttachmentBroken" = "%@-a yerreẓ, ur yezmir ara
Ad d-yettwasali ɣef Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Glem-d tawlaft i wid yesɛan ugur deg yiẓri...";
"Scene.Compose.Attachment.DescriptionVideo" = "Glem-d tavidyut i wid yesɛan ugur deg yiẓri...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "tawlaft";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "tavidyutt";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Tallunt ara yettwarnun";
"Scene.Compose.ComposeAction" = "Sufeɣ";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
index a72543a3e..10d88488a 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
@@ -68,13 +68,13 @@ Jkx girêdana înternetê xwe kontrol bike.";
"Common.Controls.Friendship.EditInfo" = "Zanyariyan serrast bike";
"Common.Controls.Friendship.Follow" = "Bişopîne";
"Common.Controls.Friendship.Following" = "Dişopîne";
-"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
+"Common.Controls.Friendship.HideReblogs" = "Bilindkirinan veşêre";
"Common.Controls.Friendship.Mute" = "Bêdeng bike";
"Common.Controls.Friendship.MuteUser" = "%@ bêdeng bike";
"Common.Controls.Friendship.Muted" = "Bêdengkirî";
"Common.Controls.Friendship.Pending" = "Tê nirxandin";
"Common.Controls.Friendship.Request" = "Daxwaz bike";
-"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
+"Common.Controls.Friendship.ShowReblogs" = "Bilindkirinan nîşan bide";
"Common.Controls.Friendship.Unblock" = "Astengiyê rake";
"Common.Controls.Friendship.UnblockUser" = "%@ asteng neke";
"Common.Controls.Friendship.Unmute" = "Bêdeng neke";
@@ -108,6 +108,10 @@ Jkx girêdana înternetê xwe kontrol bike.";
"Common.Controls.Status.Actions.Unreblog" = "Ji nû ve nivîsandinê vegere";
"Common.Controls.Status.ContentWarning" = "Hişyariya naverokê";
"Common.Controls.Status.MediaContentWarning" = "Ji bo eşkerekirinê li derekî bitikîne";
+"Common.Controls.Status.MetaEntity.Email" = "Navnîşanên e-nameyê: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtagê: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Profîlê nîşan bide: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Girêdan: %@";
"Common.Controls.Status.Poll.Closed" = "Girtî";
"Common.Controls.Status.Poll.Vote" = "Deng bide";
"Common.Controls.Status.SensitiveContent" = "Naveroka hestiyarî";
@@ -151,7 +155,7 @@ Profîla te ji wan ra wiha xuya dike.";
"Scene.AccountList.AddAccount" = "Ajimêr tevlî bike";
"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.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Şûnpel";
"Scene.Compose.Accessibility.AppendAttachment" = "Pêvek tevlî bike";
"Scene.Compose.Accessibility.AppendPoll" = "Rapirsî tevlî bike";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Hilbijêrê emojî yên kesanekirî";
@@ -161,9 +165,13 @@ Profîla te ji wan ra wiha xuya dike.";
"Scene.Compose.Accessibility.RemovePoll" = "Rapirsî rake";
"Scene.Compose.Attachment.AttachmentBroken" = "Ev %@ naxebite û nayê barkirin
li ser Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Pêvek pir mezin e";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Nikare ev pêveka medyayê nas bike";
"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.LoadFailed" = "Barkirin têk çû";
"Scene.Compose.Attachment.Photo" = "wêne";
+"Scene.Compose.Attachment.UploadFailed" = "Barkirin têk çû";
"Scene.Compose.Attachment.Video" = "vîdyo";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Bicîhkirinê tevlî bike";
"Scene.Compose.ComposeAction" = "Biweşîne";
@@ -257,12 +265,12 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin.";
"Scene.Profile.Header.FollowsYou" = "Te dişopîne";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Ji bo rakirina astengkirinê %@ bipejirîne";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Ajimêr asteng bike";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bo veşartina bilindkirinan bipejirîne";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Bilindkirinan veşêre";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Ji bo bêdengkirina %@ bipejirîne";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Ajimêrê bêdeng bike";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bo nîşandana bilindkirinan bipejirîne";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Bilindkirinan nîşan bide";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Ji bo rakirina astengkirinê %@ bipejirîne";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Astengiyê li ser ajimêr rake";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Ji bo vekirina bêdengkirinê %@ bipejirîne";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings
index 3bcc33bf5..e719583aa 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/nl.lproj/Localizable.strings
@@ -107,6 +107,10 @@
"Common.Controls.Status.Actions.Unreblog" = "Delen ongedaan maken";
"Common.Controls.Status.ContentWarning" = "Inhoudswaarschuwing";
"Common.Controls.Status.MediaContentWarning" = "Tap hier om te tonen";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Gesloten";
"Common.Controls.Status.Poll.Vote" = "Stemmen";
"Common.Controls.Status.SensitiveContent" = "Gevoelige inhoud";
@@ -155,9 +159,13 @@ Uw profiel ziet er zo uit voor hen.";
"Scene.Compose.Accessibility.PostVisibilityMenu" = "Berichtzichtbaarheidsmenu";
"Scene.Compose.Accessibility.RemovePoll" = "Peiling verwijderen";
"Scene.Compose.Attachment.AttachmentBroken" = "Deze %@ is corrupt en kan niet geüpload worden naar Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Omschrijf de foto voor mensen met een visuele beperking...";
"Scene.Compose.Attachment.DescriptionVideo" = "Omschrijf de video voor mensen met een visuele beperking...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Spaties toe te voegen";
"Scene.Compose.ComposeAction" = "Publiceren";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings
index 0513a955b..65d4fa65e 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ru.lproj/Localizable.strings
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "Убрать продвижение";
"Common.Controls.Status.ContentWarning" = "Предупреждение о содержании";
"Common.Controls.Status.MediaContentWarning" = "Нажмите в любом месте, чтобы показать";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Завершён";
"Common.Controls.Status.Poll.Vote" = "Проголосовать";
"Common.Controls.Status.SensitiveContent" = "Sensitive Content";
@@ -169,9 +173,13 @@
"Scene.Compose.Accessibility.RemovePoll" = "Убрать опрос";
"Scene.Compose.Attachment.AttachmentBroken" = "Это %@ повреждено и не может
быть отправлено в Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Опишите фото для людей с нарушениями зрения...";
"Scene.Compose.Attachment.DescriptionVideo" = "Опишите видео для людей с нарушениями зрения...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "изображение";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "видео";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Пробел, чтобы добавить";
"Scene.Compose.ComposeAction" = "Опубликовать";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings
index 849d88284..dbba5ddda 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.strings
@@ -68,13 +68,13 @@ Kontrollera din internetanslutning.";
"Common.Controls.Friendship.EditInfo" = "Redigera info";
"Common.Controls.Friendship.Follow" = "Följ";
"Common.Controls.Friendship.Following" = "Följer";
-"Common.Controls.Friendship.HideReblogs" = "Dölj puffar";
+"Common.Controls.Friendship.HideReblogs" = "Dölj boostar";
"Common.Controls.Friendship.Mute" = "Tysta";
"Common.Controls.Friendship.MuteUser" = "Tysta %@";
"Common.Controls.Friendship.Muted" = "Tystad";
"Common.Controls.Friendship.Pending" = "Väntande";
"Common.Controls.Friendship.Request" = "Följ";
-"Common.Controls.Friendship.ShowReblogs" = "Visa knuffar";
+"Common.Controls.Friendship.ShowReblogs" = "Visa boostar";
"Common.Controls.Friendship.Unblock" = "Avblockera";
"Common.Controls.Friendship.UnblockUser" = "Avblockera %@";
"Common.Controls.Friendship.Unmute" = "Avtysta";
@@ -87,27 +87,31 @@ Kontrollera din internetanslutning.";
"Common.Controls.Keyboard.SegmentedControl.PreviousSection" = "Föregående avsnitt";
"Common.Controls.Keyboard.Timeline.NextStatus" = "Nästa inlägg";
"Common.Controls.Keyboard.Timeline.OpenAuthorProfile" = "Öppna författarens profil";
-"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Öppna ompostarens profil";
+"Common.Controls.Keyboard.Timeline.OpenRebloggerProfile" = "Öppna boostarens profil";
"Common.Controls.Keyboard.Timeline.OpenStatus" = "Öppna inlägg";
"Common.Controls.Keyboard.Timeline.PreviewImage" = "Förhandsgranska bild";
"Common.Controls.Keyboard.Timeline.PreviousStatus" = "Föregående inlägg";
"Common.Controls.Keyboard.Timeline.ReplyStatus" = "Svara på inlägg";
"Common.Controls.Keyboard.Timeline.ToggleContentWarning" = "Växla innehållsvarning";
"Common.Controls.Keyboard.Timeline.ToggleFavorite" = "Växla favorit på inlägg";
-"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Växla puff på inlägg";
+"Common.Controls.Keyboard.Timeline.ToggleReblog" = "Växla boost på inlägg";
"Common.Controls.Status.Actions.Favorite" = "Favorit";
"Common.Controls.Status.Actions.Hide" = "Dölj";
"Common.Controls.Status.Actions.Menu" = "Meny";
-"Common.Controls.Status.Actions.Reblog" = "Puffa";
+"Common.Controls.Status.Actions.Reblog" = "Boosta";
"Common.Controls.Status.Actions.Reply" = "Svara";
"Common.Controls.Status.Actions.ShowGif" = "Visa GIF";
"Common.Controls.Status.Actions.ShowImage" = "Visa bild";
"Common.Controls.Status.Actions.ShowVideoPlayer" = "Visa videospelare";
"Common.Controls.Status.Actions.TapThenHoldToShowMenu" = "Tryck och håll ned för att visa menyn";
"Common.Controls.Status.Actions.Unfavorite" = "Ta bort favorit";
-"Common.Controls.Status.Actions.Unreblog" = "Ångra puff";
+"Common.Controls.Status.Actions.Unreblog" = "Ångra boost";
"Common.Controls.Status.ContentWarning" = "Innehållsvarning";
"Common.Controls.Status.MediaContentWarning" = "Tryck var som helst för att visa";
+"Common.Controls.Status.MetaEntity.Email" = "E-postadress: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Visa profil: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Länk: %@";
"Common.Controls.Status.Poll.Closed" = "Stängd";
"Common.Controls.Status.Poll.Vote" = "Rösta";
"Common.Controls.Status.SensitiveContent" = "Känsligt innehåll";
@@ -120,7 +124,7 @@ Kontrollera din internetanslutning.";
"Common.Controls.Status.Tag.Mention" = "Omnämn";
"Common.Controls.Status.Tag.Url" = "URL";
"Common.Controls.Status.TapToReveal" = "Tryck för att visa";
-"Common.Controls.Status.UserReblogged" = "%@ puffade";
+"Common.Controls.Status.UserReblogged" = "%@ boostade";
"Common.Controls.Status.UserRepliedTo" = "Svarade på %@";
"Common.Controls.Status.Visibility.Direct" = "Endast omnämnda användare kan se detta inlägg.";
"Common.Controls.Status.Visibility.Private" = "Endast deras följare kan se detta inlägg.";
@@ -151,7 +155,7 @@ Din profil ser ut så här för dem.";
"Scene.AccountList.AddAccount" = "Lägg till konto";
"Scene.AccountList.DismissAccountSwitcher" = "Stäng kontoväxlare";
"Scene.AccountList.TabBarHint" = "Nuvarande vald profil: %@. Dubbeltryck och håll för att visa kontoväxlare";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Bokmärken";
"Scene.Compose.Accessibility.AppendAttachment" = "Lägg till bilaga";
"Scene.Compose.Accessibility.AppendPoll" = "Lägg till omröstning";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Anpassad emoji-väljare";
@@ -161,9 +165,13 @@ Din profil ser ut så här för dem.";
"Scene.Compose.Accessibility.RemovePoll" = "Ta bort omröstning";
"Scene.Compose.Attachment.AttachmentBroken" = "Denna %@ är trasig och kan inte
laddas upp till Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Bilagan är för stor";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Känner inte igen mediebilagan";
"Scene.Compose.Attachment.DescriptionPhoto" = "Beskriv fotot för synskadade...";
"Scene.Compose.Attachment.DescriptionVideo" = "Beskriv videon för de synskadade...";
+"Scene.Compose.Attachment.LoadFailed" = "Det gick inte att läsa in";
"Scene.Compose.Attachment.Photo" = "foto";
+"Scene.Compose.Attachment.UploadFailed" = "Uppladdning misslyckades";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Mellanslag för att lägga till";
"Scene.Compose.ComposeAction" = "Publicera";
@@ -236,7 +244,7 @@ laddas upp till Mastodon.";
"Scene.Notification.NotificationDescription.FollowedYou" = "följde dig";
"Scene.Notification.NotificationDescription.MentionedYou" = "nämnde dig";
"Scene.Notification.NotificationDescription.PollHasEnded" = "omröstningen har avslutats";
-"Scene.Notification.NotificationDescription.RebloggedYourPost" = "puffade ditt inlägg";
+"Scene.Notification.NotificationDescription.RebloggedYourPost" = "boostade ditt inlägg";
"Scene.Notification.NotificationDescription.RequestToFollowYou" = "begär att följa dig";
"Scene.Notification.Title.Everything" = "Allting";
"Scene.Notification.Title.Mentions" = "Omnämningar";
@@ -256,12 +264,12 @@ laddas upp till Mastodon.";
"Scene.Profile.Header.FollowsYou" = "Följer dig";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "Bekräfta för att blockera %@";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "Blockera konto";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bekräfta för att dölja puffar";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Dölj puffar";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Bekräfta för att dölja boostar";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Dölj boostar";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "Bekräfta för att tysta %@";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "Tysta konto";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bekräfta för att visa puffar";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Visa puffar";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Bekräfta för att visa boostar";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Visa boostar";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "Bekräfta för att avblockera %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "Avblockera konto";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Bekräfta för att avtysta %@";
@@ -271,7 +279,7 @@ laddas upp till Mastodon.";
"Scene.Profile.SegmentedControl.Posts" = "Inlägg";
"Scene.Profile.SegmentedControl.PostsAndReplies" = "Inlägg och svar";
"Scene.Profile.SegmentedControl.Replies" = "Svar";
-"Scene.RebloggedBy.Title" = "Puffat av";
+"Scene.RebloggedBy.Title" = "Boostat av";
"Scene.Register.Error.Item.Agreement" = "Avtal";
"Scene.Register.Error.Item.Email" = "E-post";
"Scene.Register.Error.Item.Locale" = "Språk";
@@ -323,7 +331,7 @@ laddas upp till Mastodon.";
"Scene.Report.StepFinal.Unfollowed" = "Slutade följa";
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "När du ser något som du inte gillar på Mastodon kan du ta bort personen från din upplevelse.";
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Medan vi granskar detta kan du vidta åtgärder mot %@";
-"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Du kommer inte att se deras inlägg eller ompostningar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.";
+"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Du kommer inte att se deras inlägg eller boostar i ditt hemflöde. De kommer inte att veta att de har blivit tystade.";
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Finns det något annat vi borde veta?";
"Scene.Report.StepFour.Step4Of4" = "Steg 4 av 4";
"Scene.Report.StepOne.IDontLikeIt" = "Jag tycker inte om det";
@@ -414,7 +422,7 @@ laddas upp till Mastodon.";
"Scene.Settings.Section.LookAndFeel.SortaDark" = "Ganska mörk";
"Scene.Settings.Section.LookAndFeel.Title" = "Utseende och känsla";
"Scene.Settings.Section.LookAndFeel.UseSystem" = "Följ systeminställningarna";
-"Scene.Settings.Section.Notifications.Boosts" = "Ompostar mitt inlägg";
+"Scene.Settings.Section.Notifications.Boosts" = "Boostar mitt inlägg";
"Scene.Settings.Section.Notifications.Favorites" = "Favoriserar mitt inlägg";
"Scene.Settings.Section.Notifications.Follows" = "Följer mig";
"Scene.Settings.Section.Notifications.Mentions" = "Nämner mig";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict
index 048af4732..c7317903d 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/sv.lproj/Localizable.stringsdict
@@ -152,9 +152,9 @@
NSStringFormatValueTypeKey
ld
one
- %ld puff
+ %ld boost
other
- %ld puffar
+ %ld boostar
plural.count.reply
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
index 15514928c..de982308d 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
@@ -68,13 +68,13 @@
"Common.Controls.Friendship.EditInfo" = "แก้ไขข้อมูล";
"Common.Controls.Friendship.Follow" = "ติดตาม";
"Common.Controls.Friendship.Following" = "กำลังติดตาม";
-"Common.Controls.Friendship.HideReblogs" = "Hide Reblogs";
+"Common.Controls.Friendship.HideReblogs" = "ซ่อนการดัน";
"Common.Controls.Friendship.Mute" = "ซ่อน";
"Common.Controls.Friendship.MuteUser" = "ซ่อน %@";
"Common.Controls.Friendship.Muted" = "ซ่อนอยู่";
"Common.Controls.Friendship.Pending" = "รอดำเนินการ";
"Common.Controls.Friendship.Request" = "ขอ";
-"Common.Controls.Friendship.ShowReblogs" = "Show Reblogs";
+"Common.Controls.Friendship.ShowReblogs" = "แสดงการดัน";
"Common.Controls.Friendship.Unblock" = "เลิกปิดกั้น";
"Common.Controls.Friendship.UnblockUser" = "เลิกปิดกั้น %@";
"Common.Controls.Friendship.Unmute" = "เลิกซ่อน";
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "เลิกทำการดัน";
"Common.Controls.Status.ContentWarning" = "คำเตือนเนื้อหา";
"Common.Controls.Status.MediaContentWarning" = "แตะที่ใดก็ตามเพื่อเปิดเผย";
+"Common.Controls.Status.MetaEntity.Email" = "ที่อยู่อีเมล: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "แฮชแท็ก: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "โปรไฟล์ที่แสดง: %@";
+"Common.Controls.Status.MetaEntity.Url" = "ลิงก์: %@";
"Common.Controls.Status.Poll.Closed" = "ปิดแล้ว";
"Common.Controls.Status.Poll.Vote" = "ลงคะแนน";
"Common.Controls.Status.SensitiveContent" = "เนื้อหาที่ละเอียดอ่อน";
@@ -151,7 +155,7 @@
"Scene.AccountList.AddAccount" = "เพิ่มบัญชี";
"Scene.AccountList.DismissAccountSwitcher" = "ปิดตัวสลับบัญชี";
"Scene.AccountList.TabBarHint" = "โปรไฟล์ที่เลือกในปัจจุบัน: %@ แตะสองครั้งแล้วกดค้างไว้เพื่อแสดงตัวสลับบัญชี";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "ที่คั่นหน้า";
"Scene.Compose.Accessibility.AppendAttachment" = "เพิ่มไฟล์แนบ";
"Scene.Compose.Accessibility.AppendPoll" = "เพิ่มการสำรวจความคิดเห็น";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "ตัวเลือกอีโมจิที่กำหนดเอง";
@@ -161,9 +165,13 @@
"Scene.Compose.Accessibility.RemovePoll" = "เอาการสำรวจความคิดเห็นออก";
"Scene.Compose.Attachment.AttachmentBroken" = "%@ นี้เสียหายและไม่สามารถ
อัปโหลดไปยัง Mastodon";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "ไฟล์แนบใหญ่เกินไป";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "อธิบายรูปภาพสำหรับผู้บกพร่องทางการมองเห็น...";
"Scene.Compose.Attachment.DescriptionVideo" = "อธิบายวิดีโอสำหรับผู้บกพร่องทางการมองเห็น...";
+"Scene.Compose.Attachment.LoadFailed" = "การโหลดล้มเหลว";
"Scene.Compose.Attachment.Photo" = "รูปภาพ";
+"Scene.Compose.Attachment.UploadFailed" = "การอัปโหลดล้มเหลว";
"Scene.Compose.Attachment.Video" = "วิดีโอ";
"Scene.Compose.AutoComplete.SpaceToAdd" = "เว้นวรรคเพื่อเพิ่ม";
"Scene.Compose.ComposeAction" = "เผยแพร่";
@@ -256,12 +264,12 @@
"Scene.Profile.Header.FollowsYou" = "ติดตามคุณ";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Message" = "ยืนยันเพื่อปิดกั้น %@";
"Scene.Profile.RelationshipActionAlert.ConfirmBlockUser.Title" = "ปิดกั้นบัญชี";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "Confirm to hide reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "Hide Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Message" = "ยืนยันเพื่อซ่อนการดัน";
+"Scene.Profile.RelationshipActionAlert.ConfirmHideReblogs.Title" = "ซ่อนการดัน";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Message" = "ยืนยันเพื่อซ่อน %@";
"Scene.Profile.RelationshipActionAlert.ConfirmMuteUser.Title" = "ซ่อนบัญชี";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "Confirm to show reblogs";
-"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "Show Reblogs";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Message" = "ยืนยันเพื่อแสดงการดัน";
+"Scene.Profile.RelationshipActionAlert.ConfirmShowReblogs.Title" = "แสดงการดัน";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Message" = "ยืนยันเพื่อเลิกปิดกั้น %@";
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.Title" = "เลิกปิดกั้นบัญชี";
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "ยืนยันเพื่อเลิกซ่อน %@";
@@ -391,7 +399,7 @@
"Scene.ServerPicker.Label.Language" = "ภาษา";
"Scene.ServerPicker.Label.Users" = "ผู้ใช้";
"Scene.ServerPicker.Subtitle" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ";
-"Scene.ServerPicker.SubtitleExtend" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ดำเนินการโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง";
+"Scene.ServerPicker.SubtitleExtend" = "เลือกเซิร์ฟเวอร์ตามความสนใจ, ภูมิภาค หรือวัตถุประสงค์ทั่วไปของคุณ แต่ละเซิร์ฟเวอร์ได้รับการดำเนินงานโดยองค์กรหรือบุคคลที่เป็นอิสระโดยสิ้นเชิง";
"Scene.ServerPicker.Title" = "Mastodon ประกอบด้วยผู้ใช้ในเซิร์ฟเวอร์ต่าง ๆ";
"Scene.ServerRules.Button.Confirm" = "ฉันเห็นด้วย";
"Scene.ServerRules.PrivacyPolicy" = "นโยบายความเป็นส่วนตัว";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings
index eec4a2940..614ea6dc5 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/tr.lproj/Localizable.strings
@@ -107,6 +107,10 @@
"Common.Controls.Status.Actions.Unreblog" = "Yeniden paylaşımı geri al";
"Common.Controls.Status.ContentWarning" = "İçerik Uyarısı";
"Common.Controls.Status.MediaContentWarning" = "Göstermek için herhangi bir yere basın";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Kapandı";
"Common.Controls.Status.Poll.Vote" = "Oy ver";
"Common.Controls.Status.SensitiveContent" = "Hassas İçerik";
@@ -160,9 +164,13 @@ Bu kişiye göre profiliniz böyle gözüküyor.";
"Scene.Compose.Accessibility.RemovePoll" = "Anketi Kaldır";
"Scene.Compose.Attachment.AttachmentBroken" = "Bu %@ bozuk ve Mastodon'a
yüklenemiyor.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Görme engelliler için fotoğrafı tarif edin...";
"Scene.Compose.Attachment.DescriptionVideo" = "Görme engelliler için videoyu tarif edin...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "fotoğraf";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Eklemek için boşluk tuşuna basın";
"Scene.Compose.ComposeAction" = "Yayınla";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
index 14f36c7e7..41ac9aa20 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
@@ -108,6 +108,10 @@ Vui lòng kiểm tra kết nối mạng.";
"Common.Controls.Status.Actions.Unreblog" = "Hủy đăng lại";
"Common.Controls.Status.ContentWarning" = "Nội dung ẩn";
"Common.Controls.Status.MediaContentWarning" = "Nhấn để hiển thị";
+"Common.Controls.Status.MetaEntity.Email" = "Email: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Hiện hồ sơ: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "Kết thúc";
"Common.Controls.Status.Poll.Vote" = "Bình chọn";
"Common.Controls.Status.SensitiveContent" = "Nội dung nhạy cảm";
@@ -151,7 +155,7 @@ Họ sẽ thấy trang của bạn như thế này.";
"Scene.AccountList.AddAccount" = "Thêm tài khoản";
"Scene.AccountList.DismissAccountSwitcher" = "Bỏ qua chuyển đổi tài khoản";
"Scene.AccountList.TabBarHint" = "Đang dùng tài khoản: %@. Nhấn hai lần và giữ để đổi sang tài khoản khác";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "Tút đã lưu";
"Scene.Compose.Accessibility.AppendAttachment" = "Thêm media";
"Scene.Compose.Accessibility.AppendPoll" = "Tạo bình chọn";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "Chọn emoji";
@@ -161,9 +165,13 @@ Họ sẽ thấy trang của bạn như thế này.";
"Scene.Compose.Accessibility.RemovePoll" = "Xóa bình chọn";
"Scene.Compose.Attachment.AttachmentBroken" = "%@ này bị lỗi và không thể
tải lên Mastodon.";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "Mô tả hình ảnh cho người khiếm thị...";
"Scene.Compose.Attachment.DescriptionVideo" = "Mô tả video cho người khiếm thị...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "ảnh";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "video";
"Scene.Compose.AutoComplete.SpaceToAdd" = "Khoảng cách để thêm";
"Scene.Compose.ComposeAction" = "Đăng";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings
index 8e5de6b52..3883e30df 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hans.lproj/Localizable.strings
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "取消转发";
"Common.Controls.Status.ContentWarning" = "内容警告";
"Common.Controls.Status.MediaContentWarning" = "点击任意位置显示";
+"Common.Controls.Status.MetaEntity.Email" = "Email address: %@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@";
+"Common.Controls.Status.MetaEntity.Url" = "Link: %@";
"Common.Controls.Status.Poll.Closed" = "已关闭";
"Common.Controls.Status.Poll.Vote" = "投票";
"Common.Controls.Status.SensitiveContent" = "敏感内容";
@@ -161,9 +165,13 @@
"Scene.Compose.Accessibility.RemovePoll" = "移除投票";
"Scene.Compose.Attachment.AttachmentBroken" = "%@已损坏
无法上传到 Mastodon";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "为视觉障碍人士添加照片的文字说明...";
"Scene.Compose.Attachment.DescriptionVideo" = "为视觉障碍人士添加视频的文字说明...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "照片";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "视频";
"Scene.Compose.AutoComplete.SpaceToAdd" = "输入空格键入";
"Scene.Compose.ComposeAction" = "发送";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings
index f97926795..34e59a582 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/zh-Hant.lproj/Localizable.strings
@@ -108,6 +108,10 @@
"Common.Controls.Status.Actions.Unreblog" = "取消轉嘟";
"Common.Controls.Status.ContentWarning" = "內容警告";
"Common.Controls.Status.MediaContentWarning" = "輕觸任何地方以顯示";
+"Common.Controls.Status.MetaEntity.Email" = "電子郵件地址:%@";
+"Common.Controls.Status.MetaEntity.Hashtag" = "主題標籤: %@";
+"Common.Controls.Status.MetaEntity.Mention" = "顯示個人檔案:%@";
+"Common.Controls.Status.MetaEntity.Url" = "連結:%@";
"Common.Controls.Status.Poll.Closed" = "已關閉";
"Common.Controls.Status.Poll.Vote" = "投票";
"Common.Controls.Status.SensitiveContent" = "敏感內容";
@@ -147,7 +151,7 @@
"Scene.AccountList.AddAccount" = "新增帳號";
"Scene.AccountList.DismissAccountSwitcher" = "關閉帳號切換器";
"Scene.AccountList.TabBarHint" = "目前已選擇的個人檔案:%@。點兩下然後按住以顯示帳號切換器";
-"Scene.Bookmark.Title" = "Bookmarks";
+"Scene.Bookmark.Title" = "書籤";
"Scene.Compose.Accessibility.AppendAttachment" = "新增附件";
"Scene.Compose.Accessibility.AppendPoll" = "新增投票";
"Scene.Compose.Accessibility.CustomEmojiPicker" = "自訂 emoji 選擇器";
@@ -156,9 +160,13 @@
"Scene.Compose.Accessibility.PostVisibilityMenu" = "嘟文可見性選單";
"Scene.Compose.Accessibility.RemovePoll" = "移除投票";
"Scene.Compose.Attachment.AttachmentBroken" = "此 %@ 已損毀,並無法被上傳至 Mastodon。";
+"Scene.Compose.Attachment.AttachmentTooLarge" = "Attachment too large";
+"Scene.Compose.Attachment.CanNotRecognizeThisMediaAttachment" = "Can not regonize this media attachment";
"Scene.Compose.Attachment.DescriptionPhoto" = "為視障人士提供圖片說明...";
"Scene.Compose.Attachment.DescriptionVideo" = "為視障人士提供影片說明...";
+"Scene.Compose.Attachment.LoadFailed" = "Load Failed";
"Scene.Compose.Attachment.Photo" = "照片";
+"Scene.Compose.Attachment.UploadFailed" = "Upload Failed";
"Scene.Compose.Attachment.Video" = "影片";
"Scene.Compose.AutoComplete.SpaceToAdd" = "添加的空白";
"Scene.Compose.ComposeAction" = "嘟出去";
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift
index 4f8ac71d5..6c905438c 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+V2+Media.swift
@@ -43,6 +43,20 @@ extension Mastodon.API.V2.Media {
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
let serialStream = query.serialStream
request.httpBodyStream = serialStream.boundStreams.input
+
+ // total unit count in bytes count
+ // will small than actally count due to multipart protocol meta
+ serialStream.progress.totalUnitCount = {
+ var size = 0
+ size += query.file?.sizeInByte ?? 0
+ size += query.thumbnail?.sizeInByte ?? 0
+ return Int64(size)
+ }()
+ query.progress.addChild(
+ serialStream.progress,
+ withPendingUnitCount: query.progress.totalUnitCount
+ )
+
return session.dataTaskPublisher(for: request)
.tryMap { data, response in
let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response)
diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift
index f1fdac8bb..05639964e 100644
--- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift
+++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift
@@ -54,7 +54,7 @@ extension Mastodon.Query.MediaAttachment {
return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() }
}
- var sizeInByte: Int? {
+ public var sizeInByte: Int? {
switch self {
case .jpeg(let data), .gif(let data), .png(let data):
return data?.count
diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift
index 5d806b6ba..5808b9f6d 100644
--- a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift
+++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift
@@ -82,6 +82,10 @@ final class SerialStream: NSObject {
self.progress.completedUnitCount += Int64(writeResult)
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): estimate progress: \(self.progress.completedUnitCount)/\(self.progress.totalUnitCount)")
+
+ if writeResult == -1 {
+ break
+ }
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift
index ca3658e95..4c142b532 100644
--- a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift
+++ b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift
@@ -5,55 +5,55 @@
// Created by MainasuK on 22/10/10.
//
-import Foundation
+import UIKit
import MastodonCore
extension CustomEmojiPickerSection {
-// static func collectionViewDiffableDataSource(
-// collectionView: UICollectionView,
-// dependency: NeedsDependency
-// ) -> UICollectionViewDiffableDataSource {
-// let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in
-// guard let _ = dependency else { return nil }
-// switch item {
-// case .emoji(let attribute):
-// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
-// let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
-// .af.imageRounded(withCornerRadius: 4)
-//
-// let isAnimated = !UserDefaults.shared.preferredStaticEmoji
-// let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL)
-// cell.emojiImageView.sd_setImage(
-// with: url,
-// placeholderImage: placeholder,
-// options: [],
-// context: nil
-// )
-// cell.accessibilityLabel = attribute.emoji.shortcode
-// return cell
-// }
-// }
-//
-// dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in
-// guard let dataSource = dataSource else { return nil }
-// let sections = dataSource.snapshot().sectionIdentifiers
-// guard indexPath.section < sections.count else { return nil }
-// let section = sections[indexPath.section]
-//
-// switch kind {
-// case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self):
-// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
-// switch section {
-// case .emoji(let name):
-// header.titleLabel.text = name
-// }
-// return header
-// default:
-// assertionFailure()
-// return nil
-// }
-// }
-//
-// return dataSource
-// }
+ static func collectionViewDiffableDataSource(
+ collectionView: UICollectionView,
+ context: AppContext
+ ) -> UICollectionViewDiffableDataSource {
+ let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak context] collectionView, indexPath, item -> UICollectionViewCell? in
+ guard let _ = context else { return nil }
+ switch item {
+ case .emoji(let attribute):
+ let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
+ let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
+ .af.imageRounded(withCornerRadius: 4)
+
+ let isAnimated = !UserDefaults.shared.preferredStaticEmoji
+ let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL)
+ cell.emojiImageView.sd_setImage(
+ with: url,
+ placeholderImage: placeholder,
+ options: [],
+ context: nil
+ )
+ cell.accessibilityLabel = attribute.emoji.shortcode
+ return cell
+ }
+ }
+
+ dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in
+ guard let dataSource = dataSource else { return nil }
+ let sections = dataSource.snapshot().sectionIdentifiers
+ guard indexPath.section < sections.count else { return nil }
+ let section = sections[indexPath.section]
+
+ switch kind {
+ case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self):
+ let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
+ switch section {
+ case .emoji(let name):
+ header.titleLabel.text = name
+ }
+ return header
+ default:
+ assertionFailure()
+ return nil
+ }
+ }
+
+ return dataSource
+ }
}
diff --git a/MastodonSDK/Sources/MastodonUI/Extension/View.swift b/MastodonSDK/Sources/MastodonUI/Extension/View.swift
new file mode 100644
index 000000000..756e51b64
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Extension/View.swift
@@ -0,0 +1,21 @@
+//
+// View.swift
+//
+//
+// Created by MainasuK on 2022/11/8.
+//
+
+import SwiftUI
+
+extension View {
+ public func badgeView(_ content: Content) -> some View where Content: View {
+ overlay(
+ ZStack {
+ content
+ }
+ .alignmentGuide(.top) { $0.height / 2 }
+ .alignmentGuide(.trailing) { $0.width / 2 }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
+ )
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift
index f4d1397a9..9346c3bee 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift
@@ -10,237 +10,194 @@ import UIKit
import SwiftUI
import Introspect
import AVKit
+import MastodonAsset
+import MastodonLocalization
+import Introspect
public struct AttachmentView: View {
- static let size = CGSize(width: 56, height: 56)
- static let cornerRadius: CGFloat = 8
-
@ObservedObject var viewModel: AttachmentViewModel
-
- let action: (Action) -> Void
-
- @State var isCaptionEditorPresented = false
- @State var caption = ""
+
+ var blurEffect: UIBlurEffect {
+ UIBlurEffect(style: .systemUltraThinMaterialDark)
+ }
public var body: some View {
- Text("Hello")
-// Menu {
-// menu
-// } label: {
-// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
-// Image(uiImage: image)
-// .resizable()
-// .aspectRatio(contentMode: .fill)
-// .frame(width: AttachmentView.size.width, height: AttachmentView.size.height)
-// .overlay {
-// ZStack {
-// // spinner
-// if viewModel.output == nil {
-// Color.clear
-// .background(.ultraThinMaterial)
-// ProgressView()
-// .progressViewStyle(CircularProgressViewStyle())
-// .foregroundStyle(.regularMaterial)
-// }
-// // border
-// RoundedRectangle(cornerRadius: AttachmentView.cornerRadius)
-// .stroke(Color.black.opacity(0.05))
-// }
-// .transition(.opacity)
-// }
-// .overlay(alignment: .bottom) {
-// HStack(alignment: .bottom) {
-// // alt
-// VStack(spacing: 2) {
-// switch viewModel.output {
-// case .video:
-// Image(uiImage: Asset.Media.playerRectangle.image)
-// .resizable()
-// .frame(width: 16, height: 12)
-// default:
-// EmptyView()
-// }
-// if !viewModel.caption.isEmpty {
-// Image(uiImage: Asset.Media.altRectangle.image)
-// .resizable()
-// .frame(width: 16, height: 12)
-// }
-// }
-// Spacer()
-// // option
-// Image(systemName: "ellipsis")
-// .resizable()
-// .frame(width: 12, height: 12)
-// .symbolVariant(.circle)
-// .symbolVariant(.fill)
-// .symbolRenderingMode(.palette)
-// .foregroundStyle(.white, .black)
-// }
-// .padding(6)
-// }
-// .cornerRadius(AttachmentView.cornerRadius)
-// } // end Menu
-// .sheet(isPresented: $isCaptionEditorPresented) {
-// captionSheet
-// } // end caption sheet
-// .sheet(isPresented: $viewModel.isPreviewPresented) {
-// previewSheet
-// } // end preview sheet
-
+ Color.clear.aspectRatio(358.0/232.0, contentMode: .fill)
+ .overlay(
+ ZStack {
+ let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill)
+ Image(uiImage: image)
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ }
+ )
+ .overlay(
+ ZStack {
+ Color.clear
+ .overlay(
+ VStack(alignment: .leading) {
+ let placeholder: String = {
+ switch viewModel.output {
+ case .image: return L10n.Scene.Compose.Attachment.descriptionPhoto
+ case .video: return L10n.Scene.Compose.Attachment.descriptionVideo
+ case nil: return ""
+ }
+ }()
+ Spacer()
+ TextField(placeholder, text: $viewModel.caption)
+ .lineLimit(1)
+ .textFieldStyle(.plain)
+ .foregroundColor(.white)
+ .placeholder(placeholder, when: viewModel.caption.isEmpty)
+ .padding(8)
+ }
+ )
+
+ // loading…
+ if viewModel.output == nil, viewModel.error == nil {
+ ProgressView()
+ .progressViewStyle(.circular)
+ }
+
+ // load failed
+ // cannot re-entry
+ if viewModel.output == nil, let error = viewModel.error {
+ VisualEffectView(effect: blurEffect)
+ VStack {
+ Text(L10n.Scene.Compose.Attachment.loadFailed)
+ .font(.system(size: 13, weight: .semibold))
+ Text(error.localizedDescription)
+ .font(.system(size: 12, weight: .regular))
+ }
+ }
+
+ // loaded
+ // uploading… or upload failed
+ // could retry upload when error emit
+ if viewModel.output != nil, viewModel.uploadState != .finish {
+ VisualEffectView(effect: blurEffect)
+ VStack {
+ let action: AttachmentViewModel.Action = {
+ if let _ = viewModel.error {
+ return .retry
+ } else {
+ return .remove
+ }
+ }()
+ Button {
+ viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action)
+ } label: {
+ let image: UIImage = {
+ switch action {
+ case .remove:
+ return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate)
+ case .retry:
+ return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate)
+ }
+ }()
+ Image(uiImage: image)
+ .foregroundColor(.white)
+ .padding()
+ .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color))
+ .overlay(
+ Group {
+ switch viewModel.uploadState {
+ case .compressing:
+ CircleProgressView(progress: viewModel.videoCompressProgress)
+ .animation(.default, value: viewModel.videoCompressProgress)
+ case .uploading:
+ CircleProgressView(progress: viewModel.fractionCompleted)
+ .animation(.default, value: viewModel.fractionCompleted)
+ default:
+ EmptyView()
+ }
+ }
+ )
+ .clipShape(Circle())
+ .padding()
+ }
+
+ let title: String = {
+ switch action {
+ case .remove:
+ switch viewModel.uploadState {
+ case .compressing:
+ return L10n.Scene.Compose.Attachment.compressingState
+ default:
+ if viewModel.fractionCompleted < 0.9 {
+ let totalSizeInByte = viewModel.outputSizeInByte
+ let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1
+ let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte))
+ let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte))
+ return "\(upload) / \(total)"
+ } else {
+ return L10n.Scene.Compose.Attachment.serverProcessingState
+ }
+ }
+ case .retry:
+ return L10n.Scene.Compose.Attachment.uploadFailed
+ }
+ }()
+ let subtitle: String = {
+ switch action {
+ case .remove:
+ if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading {
+ if viewModel.progress.fractionCompleted < 0.9 {
+ return viewModel.remainTimeLocalizedString ?? ""
+ } else {
+ return ""
+ }
+ } else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing {
+ return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? ""
+ } else {
+ return ""
+ }
+ case .retry:
+ return viewModel.error?.localizedDescription ?? ""
+ }
+ }()
+ Text(title)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundColor(.white)
+ .padding(.horizontal)
+ Text(subtitle)
+ .font(.system(size: 12, weight: .regular))
+ .foregroundColor(.white)
+ .padding(.horizontal)
+ .lineLimit(nil)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 240)
+ }
+ }
+ } // end ZStack
+ )
} // end body
-// var menu: some View {
-// Group {
-// Button(
-// action: {
-// action(.preview)
-// },
-// label: {
-// Label(L10n.Scene.Compose.Media.preview, systemImage: "photo")
-// }
-// )
-// // caption
-// let canAddCaption: Bool = {
-// switch viewModel.output {
-// case .image: return true
-// case .video: return false
-// case .none: return false
-// }
-// }()
-// if canAddCaption {
-// Button(
-// action: {
-// action(.caption)
-// caption = viewModel.caption
-// isCaptionEditorPresented.toggle()
-// },
-// label: {
-// let title = viewModel.caption.isEmpty ? L10n.Scene.Compose.Media.Caption.add : L10n.Scene.Compose.Media.Caption.update
-// Label(title, systemImage: "text.bubble")
-// // FIXME: https://stackoverflow.com/questions/72318730/how-to-customize-swiftui-menu
-// // add caption subtitle
-// }
-// )
-// }
-// Divider()
-// // remove
-// Button(
-// role: .destructive,
-// action: {
-// action(.remove)
-// },
-// label: {
-// Label(L10n.Scene.Compose.Media.remove, systemImage: "minus.circle")
-// }
-// )
-// }
-// }
-
-// var captionSheet: some View {
-// NavigationView {
-// ScrollView(.vertical) {
-// VStack {
-// // preview
-// switch viewModel.output {
-// case .image:
-// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
-// Image(uiImage: image)
-// .resizable()
-// .aspectRatio(contentMode: .fill)
-// case .video(let url, _):
-// let player = AVPlayer(url: url)
-// VideoPlayer(player: player)
-// .frame(height: 300)
-// case .none:
-// EmptyView()
-// }
-// // caption textField
-// TextField(
-// text: $caption,
-// prompt: Text(L10n.Scene.Compose.Media.Caption.addADescriptionForThisImage)
-// ) {
-// Text(L10n.Scene.Compose.Media.Caption.update)
-// }
-// .padding()
-// .introspectTextField { textField in
-// textField.becomeFirstResponder()
-// }
-// }
-// }
-// .navigationTitle(L10n.Scene.Compose.Media.Caption.update)
-// .navigationBarTitleDisplayMode(.inline)
-// .toolbar {
-// ToolbarItem(placement: .navigationBarLeading) {
-// Button {
-// isCaptionEditorPresented.toggle()
-// } label: {
-// Image(systemName: "xmark.circle.fill")
-// .resizable()
-// .frame(width: 30, height: 30, alignment: .center)
-// .symbolRenderingMode(.hierarchical)
-// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel))
-// }
-// }
-// ToolbarItem(placement: .navigationBarTrailing) {
-// Button {
-// viewModel.caption = caption.trimmingCharacters(in: .whitespacesAndNewlines)
-// isCaptionEditorPresented.toggle()
-// } label: {
-// Text(L10n.Common.Controls.Actions.save)
-// }
-// }
-// }
-// } // end NavigationView
-// }
-
- // design for share extension
- // preferred UIKit preview in app
-// var previewSheet: some View {
-// NavigationView {
-// ScrollView(.vertical) {
-// VStack {
-// // preview
-// switch viewModel.output {
-// case .image:
-// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
-// Image(uiImage: image)
-// .resizable()
-// .aspectRatio(contentMode: .fill)
-// case .video(let url, _):
-// let player = AVPlayer(url: url)
-// VideoPlayer(player: player)
-// .frame(height: 300)
-// case .none:
-// EmptyView()
-// }
-// Spacer()
-// }
-// }
-// .navigationTitle(L10n.Scene.Compose.Media.preview)
-// .navigationBarTitleDisplayMode(.inline)
-// .toolbar {
-// ToolbarItem(placement: .navigationBarLeading) {
-// Button {
-// viewModel.isPreviewPresented.toggle()
-// } label: {
-// Image(systemName: "xmark.circle.fill")
-// .resizable()
-// .frame(width: 30, height: 30, alignment: .center)
-// .symbolRenderingMode(.hierarchical)
-// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel))
-// }
-// }
-// }
-// } // end NavigationView
-// }
-
}
-extension AttachmentView {
- public enum Action: Hashable {
- case preview
- case caption
- case remove
+// https://stackoverflow.com/a/57715771/3797903
+extension View {
+ fileprivate func placeholder(
+ when shouldShow: Bool,
+ alignment: Alignment = .leading,
+ @ViewBuilder placeholder: () -> Content) -> some View {
+
+ ZStack(alignment: alignment) {
+ placeholder().opacity(shouldShow ? 1 : 0)
+ self
+ }
+ }
+
+ fileprivate func placeholder(
+ _ text: String,
+ when shouldShow: Bool,
+ alignment: Alignment = .leading) -> some View {
+
+ placeholder(when: shouldShow, alignment: alignment) {
+ Text(text)
+ .foregroundColor(.white.opacity(0.7))
+ .lineLimit(1)
+ }
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift
new file mode 100644
index 000000000..ac1811a06
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Compress.swift
@@ -0,0 +1,94 @@
+//
+// AttachmentViewModel+Compress.swift
+//
+//
+// Created by MainasuK on 2022/11/11.
+//
+
+import os.log
+import UIKit
+import AVKit
+import SessionExporter
+import MastodonCore
+
+extension AttachmentViewModel {
+ func comporessVideo(url: URL) async throws -> URL {
+ let urlAsset = AVURLAsset(url: url)
+ let exporter = NextLevelSessionExporter(withAsset: urlAsset)
+ exporter.outputFileType = .mp4
+
+ var isLandscape: Bool = {
+ guard let track = urlAsset.tracks(withMediaType: .video).first else {
+ return true
+ }
+
+ let size = track.naturalSize.applying(track.preferredTransform)
+ return abs(size.width) >= abs(size.height)
+ }()
+
+ let outputURL = try FileManager.default.createTemporaryFileURL(
+ filename: UUID().uuidString,
+ pathExtension: url.pathExtension
+ )
+ exporter.outputURL = outputURL
+
+ let compressionDict: [String: Any] = [
+ AVVideoAverageBitRateKey: NSNumber(integerLiteral: 3000000), // 3000k
+ AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel as String,
+ AVVideoAverageNonDroppableFrameRateKey: NSNumber(floatLiteral: 30), // 30 FPS
+ ]
+ exporter.videoOutputConfiguration = [
+ AVVideoCodecKey: AVVideoCodecType.h264,
+ AVVideoWidthKey: NSNumber(integerLiteral: isLandscape ? 1280 : 720),
+ AVVideoHeightKey: NSNumber(integerLiteral: isLandscape ? 720 : 1280),
+ AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
+ AVVideoCompressionPropertiesKey: compressionDict
+ ]
+ exporter.audioOutputConfiguration = [
+ AVFormatIDKey: kAudioFormatMPEG4AAC,
+ AVEncoderBitRateKey: NSNumber(integerLiteral: 128000), // 128k
+ AVNumberOfChannelsKey: NSNumber(integerLiteral: 2),
+ AVSampleRateKey: NSNumber(value: Float(44100))
+ ]
+
+ // needs set to LOW priority to prevent priority inverse issue
+ let task = Task(priority: .utility) {
+ _ = try await exportVideo(by: exporter)
+ }
+ _ = try await task.value
+
+ return outputURL
+ }
+
+ private func exportVideo(by exporter: NextLevelSessionExporter) async throws -> URL {
+ guard let outputURL = exporter.outputURL else {
+ throw AppError.badRequest
+ }
+ return try await withCheckedThrowingContinuation { continuation in
+ exporter.export(progressHandler: { progress in
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.videoCompressProgress = Double(progress)
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: export progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
+ }
+ }, completionHandler: { result in
+ switch result {
+ case .success(let status):
+ switch status {
+ case .completed:
+ print("NextLevelSessionExporter, export completed, \(exporter.outputURL?.description ?? "")")
+ continuation.resume(with: .success(outputURL))
+ default:
+ if Task.isCancelled {
+ exporter.cancelExport()
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: cancel export", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+ print("NextLevelSessionExporter, did not complete")
+ }
+ case .failure(let error):
+ continuation.resume(with: .failure(error))
+ }
+ })
+ }
+ } // end func
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift
new file mode 100644
index 000000000..269b836bc
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+DragAndDrop.swift
@@ -0,0 +1,144 @@
+//
+// AttachmentViewModel+DragAndDrop.swift
+//
+//
+// Created by MainasuK on 2022/11/8.
+//
+
+import os.log
+import UIKit
+import Combine
+import UniformTypeIdentifiers
+
+// MARK: - TypeIdentifiedItemProvider
+extension AttachmentViewModel: TypeIdentifiedItemProvider {
+ public static var typeIdentifier: String {
+ // must in UTI format
+ // https://developer.apple.com/library/archive/qa/qa1796/_index.html
+ return "org.joinmastodon.app.AttachmentViewModel"
+ }
+}
+
+// MARK: - NSItemProviderWriting
+extension AttachmentViewModel: NSItemProviderWriting {
+
+
+ /// Attachment uniform type idendifiers
+ ///
+ /// The latest one for in-app drag and drop.
+ /// And use generic `image` and `movie` type to
+ /// allows transformable media in different formats
+ public static var writableTypeIdentifiersForItemProvider: [String] {
+ return [
+ UTType.image.identifier,
+ UTType.movie.identifier,
+ AttachmentViewModel.typeIdentifier,
+ ]
+ }
+
+ public var writableTypeIdentifiersForItemProvider: [String] {
+ // should append elements in priority order from high to low
+ var typeIdentifiers: [String] = []
+
+ // FIXME: check jpg or png
+ switch input {
+ case .image:
+ typeIdentifiers.append(UTType.png.identifier)
+ case .url(let url):
+ let _uti = UTType(filenameExtension: url.pathExtension)
+ if let uti = _uti {
+ if uti.conforms(to: .image) {
+ typeIdentifiers.append(UTType.png.identifier)
+ } else if uti.conforms(to: .movie) {
+ typeIdentifiers.append(UTType.mpeg4Movie.identifier)
+ }
+ }
+ case .pickerResult(let item):
+ if item.itemProvider.isImage() {
+ typeIdentifiers.append(UTType.png.identifier)
+ } else if item.itemProvider.isMovie() {
+ typeIdentifiers.append(UTType.mpeg4Movie.identifier)
+ }
+ case .itemProvider(let itemProvider):
+ if itemProvider.isImage() {
+ typeIdentifiers.append(UTType.png.identifier)
+ } else if itemProvider.isMovie() {
+ typeIdentifiers.append(UTType.mpeg4Movie.identifier)
+ }
+ }
+
+ typeIdentifiers.append(AttachmentViewModel.typeIdentifier)
+
+ return typeIdentifiers
+ }
+
+ public func loadData(
+ withTypeIdentifier typeIdentifier: String,
+ forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
+ ) -> Progress? {
+ switch typeIdentifier {
+ case AttachmentViewModel.typeIdentifier:
+ do {
+ let archiver = NSKeyedArchiver(requiringSecureCoding: false)
+ try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey)
+ archiver.finishEncoding()
+ let data = archiver.encodedData
+ completionHandler(data, nil)
+ } catch {
+ assertionFailure()
+ completionHandler(nil, nil)
+ }
+ default:
+ break
+ }
+
+ let loadingProgress = Progress(totalUnitCount: 100)
+
+ Publishers.CombineLatest(
+ $output,
+ $error
+ )
+ .sink { [weak self] output, error in
+ guard let self = self else { return }
+
+ // continue when load completed
+ guard output != nil || error != nil else { return }
+
+ switch output {
+ case .image(let data, _):
+ switch typeIdentifier {
+ case UTType.png.identifier:
+ loadingProgress.completedUnitCount = 100
+ completionHandler(data, nil)
+ default:
+ completionHandler(nil, nil)
+ }
+ case .video(let url, _):
+ switch typeIdentifier {
+ case UTType.png.identifier:
+ let _image = AttachmentViewModel.createThumbnailForVideo(url: url)
+ let _data = _image?.pngData()
+ loadingProgress.completedUnitCount = 100
+ completionHandler(_data, nil)
+ case UTType.mpeg4Movie.identifier:
+ let task = URLSession.shared.dataTask(with: url) { data, response, error in
+ completionHandler(data, error)
+ }
+ task.progress.observe(\.fractionCompleted) { progress, change in
+ loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted)
+ }
+ .store(in: &self.observations)
+ task.resume()
+ default:
+ completionHandler(nil, nil)
+ }
+ case nil:
+ completionHandler(nil, error)
+ }
+ }
+ .store(in: &disposeBag)
+
+ return loadingProgress
+ }
+
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift
new file mode 100644
index 000000000..a259485f1
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Load.swift
@@ -0,0 +1,148 @@
+//
+// AttachmentViewModel+Load.swift
+//
+//
+// Created by MainasuK on 2022/11/8.
+//
+
+import os.log
+import UIKit
+import AVKit
+import UniformTypeIdentifiers
+
+extension AttachmentViewModel {
+
+ @MainActor
+ func load(input: Input) async throws -> Output {
+ switch input {
+ case .image(let image):
+ guard let data = image.pngData() else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ return .image(data, imageKind: .png)
+ case .url(let url):
+ do {
+ let output = try await AttachmentViewModel.load(url: url)
+ return output
+ } catch {
+ throw error
+ }
+ case .pickerResult(let pickerResult):
+ do {
+ let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
+ return output
+ } catch {
+ throw error
+ }
+ case .itemProvider(let itemProvider):
+ do {
+ let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
+ return output
+ } catch {
+ throw error
+ }
+ }
+ }
+
+ private static func load(url: URL) async throws -> Output {
+ guard let uti = UTType(filenameExtension: url.pathExtension) else {
+ throw AttachmentError.invalidAttachmentType
+ }
+
+ if uti.conforms(to: .image) {
+ guard url.startAccessingSecurityScopedResource() else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+ let imageData = try Data(contentsOf: url)
+ return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg)
+ } else if uti.conforms(to: .movie) {
+ guard url.startAccessingSecurityScopedResource() else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+
+ let fileName = UUID().uuidString
+ let tempDirectoryURL = FileManager.default.temporaryDirectory
+ let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension)
+ try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
+ try FileManager.default.copyItem(at: url, to: fileURL)
+ return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4")
+ } else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ }
+
+ private static func load(itemProvider: NSItemProvider) async throws -> Output {
+ if itemProvider.isImage() {
+ guard let result = try await itemProvider.loadImageData() else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ let imageKind: Output.ImageKind = {
+ if let type = result.type {
+ if type == UTType.png {
+ return .png
+ }
+ if type == UTType.jpeg {
+ return .jpg
+ }
+ }
+
+ let imageData = result.data
+
+ if imageData.kf.imageFormat == .PNG {
+ return .png
+ }
+ if imageData.kf.imageFormat == .JPEG {
+ return .jpg
+ }
+
+ assertionFailure("unknown image kind")
+ return .jpg
+ }()
+ return .image(result.data, imageKind: imageKind)
+ } else if itemProvider.isMovie() {
+ guard let result = try await itemProvider.loadVideoData() else {
+ throw AttachmentError.invalidAttachmentType
+ }
+ return .video(result.url, mimeType: "video/mp4")
+ } else {
+ assertionFailure()
+ throw AttachmentError.invalidAttachmentType
+ }
+ }
+
+}
+
+extension AttachmentViewModel {
+ static func createThumbnailForVideo(url: URL) -> UIImage? {
+ guard FileManager.default.fileExists(atPath: url.path) else { return nil }
+ let asset = AVURLAsset(url: url)
+ let assetImageGenerator = AVAssetImageGenerator(asset: asset)
+ assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
+ do {
+ let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
+ let image = UIImage(cgImage: cgImage)
+ return image
+ } catch {
+ AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)")
+ return nil
+ }
+ }
+}
+
+extension NSItemProvider {
+ func isImage() -> Bool {
+ return hasRepresentationConforming(
+ toTypeIdentifier: UTType.image.identifier,
+ fileOptions: []
+ )
+ }
+
+ func isMovie() -> Bool {
+ return hasRepresentationConforming(
+ toTypeIdentifier: UTType.movie.identifier,
+ fileOptions: []
+ )
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift
index 0a4aadec3..e26e97d35 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel+Upload.swift
@@ -52,153 +52,65 @@ extension Data {
}
}
-// Twitter Only
-//extension AttachmentViewModel {
-// class SliceResult {
-//
-// let fileURL: URL
-// let chunks: Chunked
-// let chunkCount: Int
-// let type: UTType
-// let sizeInBytes: UInt64
-//
-// public init?(
-// url: URL,
-// type: UTType
-// ) {
-// guard let chunks = try? FileHandle(forReadingFrom: url).bytes.chunked else { return nil }
-// let _sizeInBytes: UInt64? = {
-// let attribute = try? FileManager.default.attributesOfItem(atPath: url.path)
-// return attribute?[.size] as? UInt64
-// }()
-// guard let sizeInBytes = _sizeInBytes else { return nil }
-//
-// self.fileURL = url
-// self.chunks = chunks
-// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes)
-// self.type = type
-// self.sizeInBytes = sizeInBytes
-// }
-//
-// public init?(
-// imageData: Data,
-// type: UTType
-// ) {
-// let _fileURL = try? FileManager.default.createTemporaryFileURL(
-// filename: UUID().uuidString,
-// pathExtension: imageData.kf.imageFormat == .PNG ? "png" : "jpeg"
-// )
-// guard let fileURL = _fileURL else { return nil }
-//
-// do {
-// try imageData.write(to: fileURL)
-// } catch {
-// return nil
-// }
-//
-// guard let chunks = try? FileHandle(forReadingFrom: fileURL).bytes.chunked else {
-// return nil
-// }
-// let sizeInBytes = UInt64(imageData.count)
-//
-// self.fileURL = fileURL
-// self.chunks = chunks
-// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes)
-// self.type = type
-// self.sizeInBytes = sizeInBytes
-// }
-//
-// static func chunkCount(chunkSize: UInt64, sizeInBytes: UInt64) -> Int {
-// guard sizeInBytes > 0 else { return 0 }
-// let count = sizeInBytes / chunkSize
-// let remains = sizeInBytes % chunkSize
-// let result = remains > 0 ? count + 1 : count
-// return Int(result)
-// }
-//
-// }
-//
-// static func slice(output: Output, sizeLimit: SizeLimit) -> SliceResult? {
-// // needs execute in background
-// assert(!Thread.isMainThread)
-//
-// // try png then use JPEG compress with Q=0.8
-// // then slice into 1MiB chunks
-// switch output {
-// case .image(let data, _):
-// let maxPayloadSizeInBytes = sizeLimit.image
-//
-// // use processed imageData to remove EXIF
-// guard let image = UIImage(data: data),
-// var imageData = image.pngData()
-// else { return nil }
-//
-// var didRemoveEXIF = false
-// repeat {
-// guard let image = KFCrossPlatformImage(data: imageData) else { return nil }
-// if imageData.kf.imageFormat == .PNG {
-// // A. png image
-// guard let pngData = image.pngData() else { return nil }
-// didRemoveEXIF = true
-// if pngData.count > maxPayloadSizeInBytes {
-// guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else { return nil }
-// os_log("%{public}s[%{public}ld], %{public}s: compress png %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024)
-// imageData = compressedJpegData
-// } else {
-// os_log("%{public}s[%{public}ld], %{public}s: png %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(pngData.count) / 1024 / 1024)
-// imageData = pngData
-// }
-// } else {
-// // B. other image
-// if !didRemoveEXIF {
-// guard let jpegData = image.jpegData(compressionQuality: 0.8) else { return nil }
-// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(jpegData.count) / 1024 / 1024)
-// imageData = jpegData
-// didRemoveEXIF = true
-// } else {
-// let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8)
-// let scaledImage = image.af.imageScaled(to: targetSize)
-// guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else { return nil }
-// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024)
-// imageData = compressedJpegData
-// }
-// }
-// } while (imageData.count > maxPayloadSizeInBytes)
-//
-// return SliceResult(
-// imageData: imageData,
-// type: imageData.kf.imageFormat == .PNG ? UTType.png : UTType.jpeg
-// )
-//
-//// case .gif(let url):
-//// fatalError()
-// case .video(let url, _):
-// return SliceResult(
-// url: url,
-// type: .movie
-// )
-// }
-// }
-//}
-
extension AttachmentViewModel {
+ public enum UploadState {
+ case none
+ case compressing
+ case ready
+ case uploading
+ case fail
+ case finish
+ }
+
struct UploadContext {
let apiService: APIService
let authContext: AuthContext
}
- enum UploadResult {
- case mastodon(Mastodon.Response.Content)
- }
+ public typealias UploadResult = Mastodon.Entity.Attachment
}
extension AttachmentViewModel {
- func upload(context: UploadContext) async throws -> UploadResult {
- return try await uploadMastodonMedia(
- context: context
- )
+ @MainActor
+ func upload(isRetry: Bool = false) async throws {
+ do {
+ let result = try await upload(
+ context: .init(
+ apiService: self.api,
+ authContext: self.authContext
+ ),
+ isRetry: isRetry
+ )
+ update(uploadResult: result)
+ } catch {
+ self.error = error
+ }
}
+ @MainActor
+ private func upload(context: UploadContext, isRetry: Bool) async throws -> UploadResult {
+ if isRetry {
+ guard uploadState == .fail else { throw AppError.badRequest }
+ self.error = nil
+ self.fractionCompleted = 0
+ } else {
+ guard uploadState == .ready else { throw AppError.badRequest }
+ }
+ do {
+ update(uploadState: .uploading)
+ let result = try await uploadMastodonMedia(
+ context: context
+ )
+ update(uploadState: .finish)
+ return result
+ } catch {
+ update(uploadState: .fail)
+ throw error
+ }
+ }
+
+ // MainActor is required here to trigger stream upload task
+ @MainActor
private func uploadMastodonMedia(
context: UploadContext
) async throws -> UploadResult {
@@ -260,7 +172,7 @@ extension AttachmentViewModel {
if attachmentUploadResponse.statusCode == 202 {
// note:
// the Mastodon server append the attachments in order by upload time
- // can not upload concurrency
+ // can not upload parallels
let waitProcessRetryLimit = checkUploadTaskRetryLimit
var waitProcessRetryCount: Int64 = 0
@@ -283,7 +195,7 @@ extension AttachmentViewModel {
// escape here
progress.completedUnitCount = progress.totalUnitCount
- return .mastodon(attachmentStatusResponse)
+ return attachmentStatusResponse.value
} else {
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
@@ -296,7 +208,7 @@ extension AttachmentViewModel {
} else {
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "")")
- return .mastodon(attachmentUploadResponse)
+ return attachmentUploadResponse.value
}
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift
index 7d0e8c859..18da157c5 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentViewModel.swift
@@ -11,36 +11,111 @@ import Combine
import PhotosUI
import Kingfisher
import MastodonCore
+import MastodonLocalization
+import func QuartzCore.CACurrentMediaTime
+
+public protocol AttachmentViewModelDelegate: AnyObject {
+ func attachmentViewModel(_ viewModel: AttachmentViewModel, uploadStateValueDidChange state: AttachmentViewModel.UploadState)
+ func attachmentViewModel(_ viewModel: AttachmentViewModel, actionButtonDidPressed action: AttachmentViewModel.Action)
+}
final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable {
static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel")
+ let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel")
public let id = UUID()
var disposeBag = Set()
var observations = Set()
+
+ weak var delegate: AttachmentViewModelDelegate?
+
+ let byteCountFormatter: ByteCountFormatter = {
+ let formatter = ByteCountFormatter()
+ formatter.allowsNonnumericFormatting = true
+ formatter.countStyle = .memory
+ return formatter
+ }()
+
+ let percentageFormatter: NumberFormatter = {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .percent
+ return formatter
+ }()
// input
+ public let api: APIService
+ public let authContext: AuthContext
public let input: Input
@Published var caption = ""
- @Published var sizeLimit = SizeLimit()
- @Published public var isPreviewPresented = false
+ // @Published var sizeLimit = SizeLimit()
// output
@Published public private(set) var output: Output?
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
- @Published var error: Error?
- let progress = Progress() // upload progress
+ @Published public private(set) var outputSizeInByte: Int64 = 0
- public init(input: Input) {
+ @Published public private(set) var uploadState: UploadState = .none
+ @Published public private(set) var uploadResult: UploadResult?
+ @Published var error: Error?
+
+ var uploadTask: Task<(), Never>?
+
+ @Published var videoCompressProgress: Double = 0
+
+ let progress = Progress() // upload progress
+ @Published var fractionCompleted: Double = 0
+
+ private var lastTimestamp: TimeInterval?
+ private var lastUploadSizeInByte: Int64 = 0
+ private var averageUploadSpeedInByte: Int64 = 0
+ private var remainTimeInterval: Double?
+ @Published var remainTimeLocalizedString: String?
+
+ public init(
+ api: APIService,
+ authContext: AuthContext,
+ input: Input,
+ delegate: AttachmentViewModelDelegate
+ ) {
+ self.api = api
+ self.authContext = authContext
self.input = input
+ self.delegate = delegate
super.init()
// end init
- defer {
- load(input: input)
- }
+ Timer.publish(every: 1.0 / 60.0, on: .main, in: .common) // 60 FPS
+ .autoconnect()
+ .share()
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ self.step()
+ }
+ .store(in: &disposeBag)
+
+ progress
+ .observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in
+ guard let self = self else { return }
+ self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)")
+ DispatchQueue.main.async {
+ self.fractionCompleted = progress.fractionCompleted
+ }
+ }
+ .store(in: &observations)
+
+ // Note: this observation is redundant if .fractionCompleted listener always emit event when reach 1.0 progress
+ // progress
+ // .observe(\.isFinished, options: [.initial, .new]) { [weak self] progress, _ in
+ // guard let self = self else { return }
+ // self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)")
+ // DispatchQueue.main.async {
+ // self.objectWillChange.send()
+ // }
+ // }
+ // .store(in: &observations)
$output
.map { output -> UIImage? in
@@ -53,22 +128,121 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
return nil
}
}
+ .receive(on: DispatchQueue.main)
.assign(to: &$thumbnail)
+
+ defer {
+ let uploadTask = Task { @MainActor in
+ do {
+ var output = try await load(input: input)
+
+ switch output {
+ case .video(let fileURL, let mimeType):
+ self.output = output
+ self.update(uploadState: .compressing)
+ let compressedFileURL = try await comporessVideo(url: fileURL)
+ output = .video(compressedFileURL, mimeType: mimeType)
+ try? FileManager.default.removeItem(at: fileURL) // remove old file
+ default:
+ break
+ }
+
+ self.outputSizeInByte = output.asAttachment.sizeInByte.flatMap { Int64($0) } ?? 0
+ self.output = output
+
+ self.update(uploadState: .ready)
+ self.delegate?.attachmentViewModel(self, uploadStateValueDidChange: self.uploadState)
+ } catch {
+ self.error = error
+ }
+ } // end Task
+ self.uploadTask = uploadTask
+ Task {
+ await uploadTask.value
+ }
+ }
}
deinit {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+
+ uploadTask?.cancel()
+
switch output {
case .image:
// FIXME:
break
case .video(let url, _):
try? FileManager.default.removeItem(at: url)
- case nil :
+ case nil:
break
}
}
}
+// calculate the upload speed
+// ref: https://stackoverflow.com/a/3841706/3797903
+extension AttachmentViewModel {
+
+ static var SpeedSmoothingFactor = 0.4
+ static let remainsTimeFormatter: RelativeDateTimeFormatter = {
+ let formatter = RelativeDateTimeFormatter()
+ formatter.unitsStyle = .full
+ return formatter
+ }()
+
+ @objc private func step() {
+
+ let uploadProgress = min(progress.fractionCompleted + 0.1, 1) // the progress split into 9:1 blocks (download : waiting)
+
+ guard let lastTimestamp = self.lastTimestamp else {
+ self.lastTimestamp = CACurrentMediaTime()
+ self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * uploadProgress)
+ return
+ }
+
+ let duration = CACurrentMediaTime() - lastTimestamp
+ guard duration >= 1.0 else { return } // update every 1 sec
+
+ let old = self.lastUploadSizeInByte
+ self.lastUploadSizeInByte = Int64(Double(outputSizeInByte) * uploadProgress)
+
+ let newSpeed = self.lastUploadSizeInByte - old
+ let lastAverageSpeed = self.averageUploadSpeedInByte
+ let newAverageSpeed = Int64(AttachmentViewModel.SpeedSmoothingFactor * Double(newSpeed) + (1 - AttachmentViewModel.SpeedSmoothingFactor) * Double(lastAverageSpeed))
+
+ let remainSizeInByte = Double(outputSizeInByte) * (1 - uploadProgress)
+
+ let speed = Double(newAverageSpeed)
+ if speed != .zero {
+ // estimate by speed
+ let uploadRemainTimeInSecond = remainSizeInByte / speed
+ // estimate by progress 1s for 10%
+ let remainPercentage = 1 - uploadProgress
+ let estimateRemainTimeByProgress = remainPercentage / 0.1
+ // max estimate
+ var remainTimeInSecond = max(estimateRemainTimeByProgress, uploadRemainTimeInSecond)
+
+ // do not increate timer when < 5 sec
+ if let remainTimeInterval = self.remainTimeInterval, remainTimeInSecond < 5 {
+ remainTimeInSecond = min(remainTimeInterval, remainTimeInSecond)
+ self.remainTimeInterval = remainTimeInSecond
+ } else {
+ self.remainTimeInterval = remainTimeInSecond
+ }
+
+ let string = AttachmentViewModel.remainsTimeFormatter.localizedString(fromTimeInterval: remainTimeInSecond)
+ remainTimeLocalizedString = string
+ // print("remains: \(remainSizeInByte), speed: \(newAverageSpeed), \(string)")
+ } else {
+ remainTimeLocalizedString = nil
+ }
+
+ self.lastTimestamp = CACurrentMediaTime()
+ self.averageUploadSpeedInByte = newAverageSpeed
+ }
+}
+
extension AttachmentViewModel {
public enum Input: Hashable {
case image(UIImage)
@@ -86,24 +260,18 @@ extension AttachmentViewModel {
case png
case jpg
}
-
- public var twitterMediaCategory: TwitterMediaCategory {
- switch self {
- case .image: return .image
- case .video: return .amplifyVideo
- }
- }
}
+ // not in using
public struct SizeLimit {
public let image: Int
public let gif: Int
public let video: Int
public init(
- image: Int = 5 * 1024 * 1024, // 5 MiB,
- gif: Int = 15 * 1024 * 1024, // 15 MiB,
- video: Int = 512 * 1024 * 1024 // 512 MiB
+ image: Int = 10 * 1024 * 1024, // 10 MiB
+ gif: Int = 40 * 1024 * 1024, // 40 MiB
+ video: Int = 40 * 1024 * 1024 // 40 MiB
) {
self.image = image
self.gif = gif
@@ -111,291 +279,38 @@ extension AttachmentViewModel {
}
}
- public enum AttachmentError: Error {
+ public enum AttachmentError: Error, LocalizedError {
case invalidAttachmentType
case attachmentTooLarge
- }
-
- public enum TwitterMediaCategory: String {
- case image = "TWEET_IMAGE"
- case GIF = "TWEET_GIF"
- case video = "TWEET_VIDEO"
- case amplifyVideo = "AMPLIFY_VIDEO"
- }
-}
-
-extension AttachmentViewModel {
-
- private func load(input: Input) {
- switch input {
- case .image(let image):
- guard let data = image.pngData() else {
- error = AttachmentError.invalidAttachmentType
- return
- }
- output = .image(data, imageKind: .png)
- case .url(let url):
- Task { @MainActor in
- do {
- let output = try await AttachmentViewModel.load(url: url)
- self.output = output
- } catch {
- self.error = error
- }
- } // end Task
- case .pickerResult(let pickerResult):
- Task { @MainActor in
- do {
- let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
- self.output = output
- } catch {
- self.error = error
- }
- } // end Task
- case .itemProvider(let itemProvider):
- Task { @MainActor in
- do {
- let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
- self.output = output
- } catch {
- self.error = error
- }
- } // end Task
- }
- }
-
- private static func load(url: URL) async throws -> Output {
- guard let uti = UTType(filenameExtension: url.pathExtension) else {
- throw AttachmentError.invalidAttachmentType
- }
- if uti.conforms(to: .image) {
- guard url.startAccessingSecurityScopedResource() else {
- throw AttachmentError.invalidAttachmentType
+ public var errorDescription: String? {
+ switch self {
+ case .invalidAttachmentType:
+ return L10n.Scene.Compose.Attachment.canNotRecognizeThisMediaAttachment
+ case .attachmentTooLarge:
+ return L10n.Scene.Compose.Attachment.attachmentTooLarge
}
- defer { url.stopAccessingSecurityScopedResource() }
- let imageData = try Data(contentsOf: url)
- return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg)
- } else if uti.conforms(to: .movie) {
- guard url.startAccessingSecurityScopedResource() else {
- throw AttachmentError.invalidAttachmentType
- }
- defer { url.stopAccessingSecurityScopedResource() }
-
- let fileName = UUID().uuidString
- let tempDirectoryURL = FileManager.default.temporaryDirectory
- let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension)
- try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
- try FileManager.default.copyItem(at: url, to: fileURL)
- return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4")
- } else {
- throw AttachmentError.invalidAttachmentType
- }
- }
-
- private static func load(itemProvider: NSItemProvider) async throws -> Output {
- if itemProvider.isImage() {
- guard let result = try await itemProvider.loadImageData() else {
- throw AttachmentError.invalidAttachmentType
- }
- let imageKind: Output.ImageKind = {
- if let type = result.type {
- if type == UTType.png {
- return .png
- }
- if type == UTType.jpeg {
- return .jpg
- }
- }
-
- let imageData = result.data
-
- if imageData.kf.imageFormat == .PNG {
- return .png
- }
- if imageData.kf.imageFormat == .JPEG {
- return .jpg
- }
-
- assertionFailure("unknown image kind")
- return .jpg
- }()
- return .image(result.data, imageKind: imageKind)
- } else if itemProvider.isMovie() {
- guard let result = try await itemProvider.loadVideoData() else {
- throw AttachmentError.invalidAttachmentType
- }
- return .video(result.url, mimeType: "video/mp4")
- } else {
- assertionFailure()
- throw AttachmentError.invalidAttachmentType
}
}
}
extension AttachmentViewModel {
- static func createThumbnailForVideo(url: URL) -> UIImage? {
- guard FileManager.default.fileExists(atPath: url.path) else { return nil }
- let asset = AVURLAsset(url: url)
- let assetImageGenerator = AVAssetImageGenerator(asset: asset)
- assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
- do {
- let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
- let image = UIImage(cgImage: cgImage)
- return image
- } catch {
- AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)")
- return nil
- }
+ public enum Action: Hashable {
+ case remove
+ case retry
}
}
-// MARK: - TypeIdentifiedItemProvider
-extension AttachmentViewModel: TypeIdentifiedItemProvider {
- public static var typeIdentifier: String {
- // must in UTI format
- // https://developer.apple.com/library/archive/qa/qa1796/_index.html
- return "com.twidere.AttachmentViewModel"
- }
-}
-
-// MARK: - NSItemProviderWriting
-extension AttachmentViewModel: NSItemProviderWriting {
-
-
- /// Attachment uniform type idendifiers
- ///
- /// The latest one for in-app drag and drop.
- /// And use generic `image` and `movie` type to
- /// allows transformable media in different formats
- public static var writableTypeIdentifiersForItemProvider: [String] {
- return [
- UTType.image.identifier,
- UTType.movie.identifier,
- AttachmentViewModel.typeIdentifier,
- ]
- }
-
- public var writableTypeIdentifiersForItemProvider: [String] {
- // should append elements in priority order from high to low
- var typeIdentifiers: [String] = []
-
- // FIXME: check jpg or png
- switch input {
- case .image:
- typeIdentifiers.append(UTType.png.identifier)
- case .url(let url):
- let _uti = UTType(filenameExtension: url.pathExtension)
- if let uti = _uti {
- if uti.conforms(to: .image) {
- typeIdentifiers.append(UTType.png.identifier)
- } else if uti.conforms(to: .movie) {
- typeIdentifiers.append(UTType.mpeg4Movie.identifier)
- }
- }
- case .pickerResult(let item):
- if item.itemProvider.isImage() {
- typeIdentifiers.append(UTType.png.identifier)
- } else if item.itemProvider.isMovie() {
- typeIdentifiers.append(UTType.mpeg4Movie.identifier)
- }
- case .itemProvider(let itemProvider):
- if itemProvider.isImage() {
- typeIdentifiers.append(UTType.png.identifier)
- } else if itemProvider.isMovie() {
- typeIdentifiers.append(UTType.mpeg4Movie.identifier)
- }
- }
-
- typeIdentifiers.append(AttachmentViewModel.typeIdentifier)
-
- return typeIdentifiers
- }
-
- public func loadData(
- withTypeIdentifier typeIdentifier: String,
- forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
- ) -> Progress? {
- switch typeIdentifier {
- case AttachmentViewModel.typeIdentifier:
- do {
- let archiver = NSKeyedArchiver(requiringSecureCoding: false)
- try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey)
- archiver.finishEncoding()
- let data = archiver.encodedData
- completionHandler(data, nil)
- } catch {
- assertionFailure()
- completionHandler(nil, nil)
- }
- default:
- break
- }
-
- let loadingProgress = Progress(totalUnitCount: 100)
-
- Publishers.CombineLatest(
- $output,
- $error
- )
- .sink { [weak self] output, error in
- guard let self = self else { return }
-
- // continue when load completed
- guard output != nil || error != nil else { return }
-
- switch output {
- case .image(let data, _):
- switch typeIdentifier {
- case UTType.png.identifier:
- loadingProgress.completedUnitCount = 100
- completionHandler(data, nil)
- default:
- completionHandler(nil, nil)
- }
- case .video(let url, _):
- switch typeIdentifier {
- case UTType.png.identifier:
- let _image = AttachmentViewModel.createThumbnailForVideo(url: url)
- let _data = _image?.pngData()
- loadingProgress.completedUnitCount = 100
- completionHandler(_data, nil)
- case UTType.mpeg4Movie.identifier:
- let task = URLSession.shared.dataTask(with: url) { data, response, error in
- completionHandler(data, error)
- }
- task.progress.observe(\.fractionCompleted) { progress, change in
- loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted)
- }
- .store(in: &self.observations)
- task.resume()
- default:
- completionHandler(nil, nil)
- }
- case nil:
- completionHandler(nil, error)
- }
- }
- .store(in: &disposeBag)
-
- return loadingProgress
- }
-
-}
-
-extension NSItemProvider {
- fileprivate func isImage() -> Bool {
- return hasRepresentationConforming(
- toTypeIdentifier: UTType.image.identifier,
- fileOptions: []
- )
- }
-
- fileprivate func isMovie() -> Bool {
- return hasRepresentationConforming(
- toTypeIdentifier: UTType.movie.identifier,
- fileOptions: []
- )
+extension AttachmentViewModel {
+ @MainActor
+ func update(uploadState: UploadState) {
+ self.uploadState = uploadState
+ self.delegate?.attachmentViewModel(self, uploadStateValueDidChange: self.uploadState)
+ }
+
+ @MainActor
+ func update(uploadResult: UploadResult) {
+ self.uploadResult = uploadResult
}
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift
index aa21057d1..9af1ce9bf 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift
@@ -88,7 +88,7 @@ extension AutoCompleteViewController {
])
tableView.delegate = self
-// viewModel.setupDiffableDataSource(tableView: tableView)
+ viewModel.setupDiffableDataSource(tableView: tableView)
// bind to layout chevron
viewModel.symbolBoundingRect
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift
index adbf6ac09..2dd815d0a 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift
@@ -6,17 +6,18 @@
//
import UIKit
+import MastodonCore
extension AutoCompleteViewModel {
-// func setupDiffableDataSource(
-// tableView: UITableView
-// ) {
-// diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView)
-//
-// var snapshot = NSDiffableDataSourceSnapshot()
-// snapshot.appendSections([.main])
-// diffableDataSource?.apply(snapshot)
-// }
+ func setupDiffableDataSource(
+ tableView: UITableView
+ ) {
+ diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(tableView: tableView)
+
+ var snapshot = NSDiffableDataSourceSnapshot()
+ snapshot.appendSections([.main])
+ diffableDataSource?.apply(snapshot)
+ }
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift
index b1f5f3187..7f93c4ba7 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift
@@ -102,7 +102,7 @@ extension AutoCompleteViewModel.State {
return
}
- guard let customEmojiViewModel = viewModel.customEmojiViewModel.value else {
+ guard let customEmojiViewModel = viewModel.customEmojiViewModel else {
await enter(state: Fail.self)
return
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift
index 61715cd63..d8fa06db6 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift
@@ -20,7 +20,7 @@ final class AutoCompleteViewModel {
let authContext: AuthContext
public let inputText = CurrentValueSubject("") // contains "@" or "#" prefix
public let symbolBoundingRect = CurrentValueSubject(.zero)
- public let customEmojiViewModel = CurrentValueSubject(nil)
+ public let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
// output
public var autoCompleteItems = CurrentValueSubject<[AutoCompleteItem], Never>([])
@@ -40,6 +40,8 @@ final class AutoCompleteViewModel {
init(context: AppContext, authContext: AuthContext) {
self.context = context
self.authContext = authContext
+ self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain)
+ // end init
autoCompleteItems
.receive(on: DispatchQueue.main)
@@ -71,7 +73,7 @@ final class AutoCompleteViewModel {
inputText
.removeDuplicates()
- .receive(on: DispatchQueue.main)
+ .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] inputText in
guard let self = self else { return }
self.stateMachine.enter(State.Loading.self)
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift
index ccc36b1df..6b842c9f1 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift
@@ -8,7 +8,6 @@
import UIKit
import Combine
import MastodonCore
-import MastodonUI
final class AutoCompleteTopChevronView: UIView {
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift
index 3417ed935..b84b47385 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift
@@ -14,12 +14,15 @@ import MastodonCore
public final class ComposeContentViewController: UIViewController {
+ static let minAutoCompleteVisibleHeight: CGFloat = 100
+
let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController")
var disposeBag = Set()
public var viewModel: ComposeContentViewModel!
private(set) lazy var composeContentToolbarViewModel = ComposeContentToolbarView.ViewModel(delegate: self)
+ // tableView container
let tableView: ComposeTableView = {
let tableView = ComposeTableView()
tableView.estimatedRowHeight = UITableView.automaticDimension
@@ -29,6 +32,16 @@ public final class ComposeContentViewController: UIViewController {
return tableView
}()
+ // auto complete
+ private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
+ let viewController = AutoCompleteViewController()
+ viewController.viewModel = AutoCompleteViewModel(context: viewModel.context, authContext: viewModel.authContext)
+ viewController.delegate = self
+ // viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
+ return viewController
+ }()
+
+ // toolbar
lazy var composeContentToolbarView = ComposeContentToolbarView(viewModel: composeContentToolbarViewModel)
var composeContentToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
let composeContentToolbarBackgroundView = UIView()
@@ -42,24 +55,33 @@ public final class ComposeContentViewController: UIViewController {
return configuration
}
- private(set) lazy var photoLibraryPicker: PHPickerViewController = {
+ public private(set) lazy var photoLibraryPicker: PHPickerViewController = {
let imagePicker = PHPickerViewController(configuration: ComposeContentViewController.createPhotoLibraryPickerConfiguration())
imagePicker.delegate = self
return imagePicker
}()
- private(set) lazy var imagePickerController: UIImagePickerController = {
+ public private(set) lazy var imagePickerController: UIImagePickerController = {
let imagePickerController = UIImagePickerController()
imagePickerController.sourceType = .camera
imagePickerController.delegate = self
return imagePickerController
}()
- private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
+ public private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie])
documentPickerController.delegate = self
return documentPickerController
}()
+
+ // emoji picker inputView
+ let customEmojiPickerInputView: CustomEmojiPickerInputView = {
+ let view = CustomEmojiPickerInputView(
+ frame: CGRect(x: 0, y: 0, width: 0, height: 300),
+ inputViewStyle: .keyboard
+ )
+ return view
+ }()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
@@ -71,6 +93,8 @@ extension ComposeContentViewController {
public override func viewDidLoad() {
super.viewDidLoad()
+ viewModel.delegate = self
+
// setup view
self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
@@ -94,6 +118,12 @@ extension ComposeContentViewController {
tableView.delegate = self
viewModel.setupDataSource(tableView: tableView)
+ // setup emoji picker
+ customEmojiPickerInputView.collectionView.delegate = self
+ viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
+ viewModel.setupCustomEmojiPickerDiffableDataSource(collectionView: customEmojiPickerInputView.collectionView)
+
+ // setup toolbar
let toolbarHostingView = UIHostingController(rootView: composeContentToolbarView)
toolbarHostingView.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(toolbarHostingView.view)
@@ -116,49 +146,43 @@ extension ComposeContentViewController {
view.bottomAnchor.constraint(equalTo: composeContentToolbarBackgroundView.bottomAnchor),
])
- let keyboardHasShortcutBar = CurrentValueSubject(traitCollection.userInterfaceIdiom == .pad) // update default value later
+ // bind keyboard
let keyboardEventPublishers = Publishers.CombineLatest3(
KeyboardResponderService.shared.isShow,
KeyboardResponderService.shared.state,
KeyboardResponderService.shared.endFrame
)
-// Publishers.CombineLatest3(
-// viewModel.$isCustomEmojiComposing,
-// )
- keyboardEventPublishers
- .sink(receiveValue: { [weak self] keyboardEvents in
+ Publishers.CombineLatest3(
+ keyboardEventPublishers,
+ viewModel.$isEmojiActive,
+ viewModel.$autoCompleteInfo
+ )
+ .sink(receiveValue: { [weak self] keyboardEvents, isEmojiActive, autoCompleteInfo in
guard let self = self else { return }
let (isShow, state, endFrame) = keyboardEvents
-
-// switch self.traitCollection.userInterfaceIdiom {
-// case .pad:
-// keyboardHasShortcutBar.value = state != .floating
-// default:
-// keyboardHasShortcutBar.value = false
-// }
-//
+
let extraMargin: CGFloat = {
var margin = ComposeContentToolbarView.toolbarHeight
-// if autoCompleteInfo != nil {
-//// margin += ComposeViewController.minAutoCompleteVisibleHeight
-// }
+ if autoCompleteInfo != nil {
+ margin += ComposeContentViewController.minAutoCompleteVisibleHeight
+ }
return margin
}()
-//
+
guard isShow, state == .dock else {
self.tableView.contentInset.bottom = extraMargin
self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
-// if let superView = self.autoCompleteViewController.tableView.superview {
-// let autoCompleteTableViewBottomInset: CGFloat = {
-// let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
-// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
-// return max(0, padding)
-// }()
-// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
-// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
-// }
+ if let superView = self.autoCompleteViewController.tableView.superview {
+ let autoCompleteTableViewBottomInset: CGFloat = {
+ let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
+ let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
+ return max(0, padding)
+ }()
+ self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
+ self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
+ }
UIView.animate(withDuration: 0.3) {
self.composeContentToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
@@ -169,17 +193,16 @@ extension ComposeContentViewController {
return
}
// isShow AND dock state
-// self.systemKeyboardHeight = endFrame.height
// adjust inset for auto-complete
-// let autoCompleteTableViewBottomInset: CGFloat = {
-// guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
-// let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
-// let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY
-// return max(0, padding)
-// }()
-// self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
-// self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
+ let autoCompleteTableViewBottomInset: CGFloat = {
+ guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
+ let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
+ let padding = tableViewFrameInWindow.maxY + ComposeContentToolbarView.toolbarHeight + AutoCompleteViewController.chevronViewHeight - endFrame.minY
+ return max(0, padding)
+ }()
+ self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
+ self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
// adjust inset for tableView
let contentFrame = self.view.convert(self.tableView.frame, to: nil)
@@ -218,14 +241,63 @@ extension ComposeContentViewController {
}
.store(in: &disposeBag)
+ // bind auto-complete
+ viewModel.$autoCompleteInfo
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] info in
+ guard let self = self else { return }
+ guard let textView = self.viewModel.contentMetaText?.textView else { return }
+ if self.autoCompleteViewController.view.superview == nil {
+ self.autoCompleteViewController.view.frame = self.view.bounds
+ // add to container view. seealso: `viewDidLayoutSubviews()`
+ self.viewModel.composeContentTableViewCell.contentView.addSubview(self.autoCompleteViewController.view)
+ self.addChild(self.autoCompleteViewController)
+ self.autoCompleteViewController.didMove(toParent: self)
+ self.autoCompleteViewController.view.isHidden = true
+ self.tableView.autoCompleteViewController = self.autoCompleteViewController
+ }
+ self.updateAutoCompleteViewControllerLayout()
+ self.autoCompleteViewController.view.isHidden = info == nil
+ guard let info = info else { return }
+ let symbolBoundingRectInContainer = textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
+ print(info.symbolBoundingRect)
+ self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY + self.viewModel.contentTextViewFrame.minY
+ self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
+ self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
+ }
+ .store(in: &disposeBag)
+
+ // bind emoji picker
+ viewModel.customEmojiViewModel?.emojis
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: { [weak self] emojis in
+ guard let self = self else { return }
+ if emojis.isEmpty {
+ self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
+ } else {
+ self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
+ }
+ })
+ .store(in: &disposeBag)
+
// bind toolbar
bindToolbarViewModel()
+
+ // bind attachment picker
+ viewModel.$attachmentViewModels
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ self.resetImagePicker()
+ }
+ .store(in: &disposeBag)
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
viewModel.viewLayoutFrame.update(view: view)
+ updateAutoCompleteViewControllerLayout()
}
public override func viewSafeAreaInsetsDidChange() {
@@ -257,12 +329,50 @@ extension ComposeContentViewController {
}
private func bindToolbarViewModel() {
+ viewModel.$isAttachmentButtonEnabled.assign(to: &composeContentToolbarViewModel.$isAttachmentButtonEnabled)
+ viewModel.$isPollButtonEnabled.assign(to: &composeContentToolbarViewModel.$isPollButtonEnabled)
viewModel.$isPollActive.assign(to: &composeContentToolbarViewModel.$isPollActive)
viewModel.$isEmojiActive.assign(to: &composeContentToolbarViewModel.$isEmojiActive)
viewModel.$isContentWarningActive.assign(to: &composeContentToolbarViewModel.$isContentWarningActive)
+ viewModel.$visibility.assign(to: &composeContentToolbarViewModel.$visibility)
viewModel.$maxTextInputLimit.assign(to: &composeContentToolbarViewModel.$maxTextInputLimit)
viewModel.$contentWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWeightedLength)
viewModel.$contentWarningWeightedLength.assign(to: &composeContentToolbarViewModel.$contentWarningWeightedLength)
+
+ // bind back to source due to visibility not update via delegate
+ composeContentToolbarViewModel.$visibility
+ .dropFirst()
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] visibility in
+ guard let self = self else { return }
+ if self.viewModel.visibility != visibility {
+ self.viewModel.visibility = visibility
+ }
+ }
+ .store(in: &disposeBag)
+ }
+
+ private func updateAutoCompleteViewControllerLayout() {
+ // pin autoCompleteViewController frame to current view
+ if let containerView = autoCompleteViewController.view.superview {
+ let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
+ if viewFrameInWindow.origin.x != 0 {
+ autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
+ }
+ autoCompleteViewController.view.frame.size.width = view.frame.width
+ }
+ }
+
+ private func resetImagePicker() {
+ let selectionLimit = max(1, viewModel.maxMediaAttachmentLimit - viewModel.attachmentViewModels.count)
+ let configuration = ComposeContentViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
+ photoLibraryPicker = createImagePicker(configuration: configuration)
+ }
+
+ private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
+ let imagePicker = PHPickerViewController(configuration: configuration)
+ imagePicker.delegate = self
+ return imagePicker
}
}
@@ -325,16 +435,15 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate {
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true, completion: nil)
- // TODO:
-// let attachmentServices: [MastodonAttachmentService] = results.map { result in
-// let service = MastodonAttachmentService(
-// context: context,
-// pickerResult: result,
-// initialAuthenticationBox: viewModel.authenticationBox
-// )
-// return service
-// }
-// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices
+ let attachmentViewModels: [AttachmentViewModel] = results.map { result in
+ AttachmentViewModel(
+ api: viewModel.context.apiService,
+ authContext: viewModel.authContext,
+ input: .pickerResult(result),
+ delegate: viewModel
+ )
+ }
+ viewModel.attachmentViewModels += attachmentViewModels
}
}
@@ -345,12 +454,13 @@ extension ComposeContentViewController: UIImagePickerControllerDelegate & UINavi
guard let image = info[.originalImage] as? UIImage else { return }
-// let attachmentService = MastodonAttachmentService(
-// context: context,
-// image: image,
-// initialAuthenticationBox: viewModel.authenticationBox
-// )
-// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService]
+ let attachmentViewModel = AttachmentViewModel(
+ api: viewModel.context.apiService,
+ authContext: viewModel.authContext,
+ input: .image(image),
+ delegate: viewModel
+ )
+ viewModel.attachmentViewModels += [attachmentViewModel]
}
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
@@ -364,12 +474,13 @@ extension ComposeContentViewController: UIDocumentPickerDelegate {
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return }
-// let attachmentService = MastodonAttachmentService(
-// context: context,
-// documentURL: url,
-// initialAuthenticationBox: viewModel.authenticationBox
-// )
-// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService]
+ let attachmentViewModel = AttachmentViewModel(
+ api: viewModel.context.apiService,
+ authContext: viewModel.authContext,
+ input: .url(url),
+ delegate: viewModel
+ )
+ viewModel.attachmentViewModels += [attachmentViewModel]
}
}
@@ -428,3 +539,123 @@ extension ComposeContentViewController: ComposeContentToolbarViewDelegate {
}
}
}
+
+// MARK: - AutoCompleteViewControllerDelegate
+extension ComposeContentViewController: AutoCompleteViewControllerDelegate {
+ func autoCompleteViewController(
+ _ viewController: AutoCompleteViewController,
+ didSelectItem item: AutoCompleteItem
+ ) {
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did select item: \(String(describing: item))")
+
+ guard let info = viewModel.autoCompleteInfo else { return }
+ guard let metaText = viewModel.contentMetaText else { return }
+
+ let _replacedText: String? = {
+ var text: String
+ switch item {
+ case .hashtag(let hashtag):
+ text = "#" + hashtag.name
+ case .hashtagV1(let hashtagName):
+ text = "#" + hashtagName
+ case .account(let account):
+ text = "@" + account.acct
+ case .emoji(let emoji):
+ text = ":" + emoji.shortcode + ":"
+ case .bottomLoader:
+ return nil
+ }
+ return text
+ }()
+ guard let replacedText = _replacedText else { return }
+ guard let text = metaText.textView.text else { return }
+
+ let range = NSRange(info.toHighlightEndRange, in: text)
+ metaText.textStorage.replaceCharacters(in: range, with: replacedText)
+ viewModel.autoCompleteInfo = nil
+
+ // set selected range
+ let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
+ guard metaText.textStorage.length <= newRange.location else { return }
+ metaText.textView.selectedRange = newRange
+
+ // append a space and trigger textView delegate update
+ DispatchQueue.main.async {
+ metaText.textView.insertText(" ")
+ }
+ }
+}
+
+// MARK: - UICollectionViewDelegate
+extension ComposeContentViewController: UICollectionViewDelegate {
+
+ public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
+
+ switch collectionView {
+ case customEmojiPickerInputView.collectionView:
+ guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
+ let item = diffableDataSource.itemIdentifier(for: indexPath)
+ guard case let .emoji(attribute) = item else { return }
+ let emoji = attribute.emoji
+
+ // make click sound
+ UIDevice.current.playInputClick()
+
+ // retrieve active text input and insert emoji
+ // the trailing space is REQUIRED to make regex happy
+ _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ")
+ default:
+ assertionFailure()
+ }
+ } // end func
+
+}
+
+// MARK: - ComposeContentViewModelDelegate
+extension ComposeContentViewController: ComposeContentViewModelDelegate {
+ public func composeContentViewModel(
+ _ viewModel: ComposeContentViewModel,
+ handleAutoComplete info: ComposeContentViewModel.AutoCompleteInfo
+ ) -> Bool {
+ let snapshot = autoCompleteViewController.viewModel.diffableDataSource.snapshot()
+ guard let item = snapshot.itemIdentifiers.first else { return false }
+
+ // FIXME: redundant code
+ guard let metaText = viewModel.contentMetaText else { return false }
+ guard let text = metaText.textView.text else { return false }
+ let _replacedText: String? = {
+ var text: String
+ switch item {
+ case .hashtag(let hashtag):
+ text = "#" + hashtag.name
+ case .hashtagV1(let hashtagName):
+ text = "#" + hashtagName
+ case .account(let account):
+ text = "@" + account.acct
+ case .emoji(let emoji):
+ text = ":" + emoji.shortcode + ":"
+ case .bottomLoader:
+ return nil
+ }
+ return text
+ }()
+ guard let replacedText = _replacedText else { return false }
+
+ let range = NSRange(info.toHighlightEndRange, in: text)
+ metaText.textStorage.replaceCharacters(in: range, with: replacedText)
+ viewModel.autoCompleteInfo = nil
+
+ // set selected range
+ let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
+ guard metaText.textStorage.length <= newRange.location else { return true }
+ metaText.textView.selectedRange = newRange
+
+ // append a space and trigger textView delegate update
+ DispatchQueue.main.async {
+ metaText.textView.insertText(" ")
+ }
+
+ return true
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift
index 3f6028b56..abbfe0e61 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift
@@ -66,14 +66,15 @@ extension ComposeContentViewModel {
guard let replyTo = status.object(in: context.managedObjectContext) else { return }
cell.statusView.configure(status: replyTo)
}
- case .hashtag(let hashtag):
+ case .hashtag:
break
- case .mention(let user):
+ case .mention:
break
}
}
}
+// MARK: - UITableViewDataSource
extension ComposeContentViewModel: UITableViewDataSource {
public func numberOfSections(in tableView: UITableView) -> Int {
return Section.allCases.count
@@ -99,3 +100,42 @@ extension ComposeContentViewModel: UITableViewDataSource {
}
}
}
+
+extension ComposeContentViewModel {
+
+ func setupCustomEmojiPickerDiffableDataSource(
+ collectionView: UICollectionView
+ ) {
+ let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
+ collectionView: collectionView,
+ context: context
+ )
+ self.customEmojiPickerDiffableDataSource = diffableDataSource
+
+ let domain = authContext.mastodonAuthenticationBox.domain.uppercased()
+ customEmojiViewModel?.emojis
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self, weak diffableDataSource] emojis in
+ guard let _ = self else { return }
+ guard let diffableDataSource = diffableDataSource else { return }
+
+ var snapshot = NSDiffableDataSourceSnapshot()
+ let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
+ snapshot.appendSections([customEmojiSection])
+ let items: [CustomEmojiPickerItem] = {
+ var items = [CustomEmojiPickerItem]()
+ for emoji in emojis where emoji.visibleInPicker {
+ let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
+ let item = CustomEmojiPickerItem.emoji(attribute: attribute)
+ items.append(item)
+ }
+ return items
+ }()
+ snapshot.appendItems(items, toSection: customEmojiSection)
+
+ diffableDataSource.apply(snapshot)
+ }
+ .store(in: &disposeBag)
+ }
+
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift
index 80cc033e8..8a189739d 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+MetaTextDelegate.swift
@@ -37,7 +37,7 @@ extension ComposeContentViewModel: MetaTextDelegate {
let content = MastodonContent(
content: textInput,
- emojis: [:] // TODO: emojiViewModel?.emojis.asDictionary ?? [:]
+ emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent
@@ -48,7 +48,7 @@ extension ComposeContentViewModel: MetaTextDelegate {
let content = MastodonContent(
content: textInput,
- emojis: [:] // emojiViewModel?.emojis.asDictionary ?? [:]
+ emojis: [:] // customEmojiViewModel?.emojis.value.asDictionary ?? [:]
)
let metaContent = MastodonMetaContent.convert(text: content)
return metaContent
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift
new file mode 100644
index 000000000..cdf322a38
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+UITextViewDelegate.swift
@@ -0,0 +1,209 @@
+//
+// ComposeContentViewModel+UITextViewDelegate.swift
+//
+//
+// Created by MainasuK on 2022/11/13.
+//
+
+import os.log
+import UIKit
+
+// MARK: - UITextViewDelegate
+extension ComposeContentViewModel: UITextViewDelegate {
+
+ public func textViewDidBeginEditing(_ textView: UITextView) {
+ // Note:
+ // Xcode warning:
+ // Publishing changes from within view updates is not allowed, this will cause undefined behavior.
+ //
+ // Just ignore the warning and see what will happen…
+ switch textView {
+ case contentMetaText?.textView:
+ isContentEditing = true
+ case contentWarningMetaText?.textView:
+ isContentWarningEditing = true
+ default:
+ assertionFailure()
+ break
+ }
+ }
+
+ public func textViewDidChange(_ textView: UITextView) {
+ switch textView {
+ case contentMetaText?.textView:
+ // update model
+ guard let metaText = self.contentMetaText else {
+ assertionFailure()
+ return
+ }
+ let backedString = metaText.backedString
+ logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
+
+ // configure auto completion
+ setupAutoComplete(for: textView)
+
+ case contentWarningMetaText?.textView:
+ break
+ default:
+ assertionFailure()
+ }
+ }
+
+ public func textViewDidEndEditing(_ textView: UITextView) {
+ switch textView {
+ case contentMetaText?.textView:
+ isContentEditing = false
+ case contentWarningMetaText?.textView:
+ isContentWarningEditing = false
+ default:
+ assertionFailure()
+ break
+ }
+ }
+
+ public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
+ switch textView {
+ case contentMetaText?.textView:
+ if text == " ", let autoCompleteInfo = self.autoCompleteInfo {
+ assert(delegate != nil)
+ let isHandled = delegate?.composeContentViewModel(self, handleAutoComplete: autoCompleteInfo) ?? false
+ return !isHandled
+ }
+
+ return true
+ case contentWarningMetaText?.textView:
+ let isReturn = text == "\n"
+ if isReturn {
+ setContentTextViewFirstResponderIfNeeds()
+ }
+ return !isReturn
+ default:
+ assertionFailure()
+ return true
+ }
+ }
+
+}
+
+extension ComposeContentViewModel {
+
+ func insertContentText(text: String) {
+ guard let contentMetaText = self.contentMetaText else { return }
+ // FIXME: smart prefix and suffix
+ let string = contentMetaText.textStorage.string
+ let isEmpty = string.isEmpty
+ let hasPrefix = string.hasPrefix(" ")
+ if hasPrefix || isEmpty {
+ contentMetaText.textView.insertText(text)
+ } else {
+ contentMetaText.textView.insertText(" " + text)
+ }
+ }
+
+ func setContentTextViewFirstResponderIfNeeds() {
+ guard let contentMetaText = self.contentMetaText else { return }
+ guard !contentMetaText.textView.isFirstResponder else { return }
+ contentMetaText.textView.becomeFirstResponder()
+ }
+
+ func setContentWarningTextViewFirstResponderIfNeeds() {
+ guard let contentWarningMetaText = self.contentWarningMetaText else { return }
+ guard !contentWarningMetaText.textView.isFirstResponder else { return }
+ contentWarningMetaText.textView.becomeFirstResponder()
+ }
+
+}
+
+extension ComposeContentViewModel {
+
+ private func setupAutoComplete(for textView: UITextView) {
+ guard var autoCompletion = ComposeContentViewModel.scanAutoCompleteInfo(textView: textView) else {
+ self.autoCompleteInfo = nil
+ return
+ }
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))
+
+ // get layout text bounding rect
+ var glyphRange = NSRange()
+ textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
+ let textContainer = textView.layoutManager.textContainers[0]
+ let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
+
+ let retryLayoutTimes = autoCompleteRetryLayoutTimes
+ guard textBoundingRect.size != .zero else {
+ autoCompleteRetryLayoutTimes += 1
+ // avoid infinite loop
+ guard retryLayoutTimes < 3 else { return }
+ // needs retry calculate layout when the rect position changing
+ DispatchQueue.main.async {
+ self.setupAutoComplete(for: textView)
+ }
+ return
+ }
+ autoCompleteRetryLayoutTimes = 0
+
+ // get symbol bounding rect
+ textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
+ let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
+
+ // set bounding rect and trigger layout
+ autoCompletion.textBoundingRect = textBoundingRect
+ autoCompletion.symbolBoundingRect = symbolBoundingRect
+ autoCompleteInfo = autoCompletion
+ }
+
+ private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
+ guard let text = textView.text,
+ textView.selectedRange.location > 0, !text.isEmpty,
+ let selectedRange = Range(textView.selectedRange, in: text) else {
+ return nil
+ }
+ let cursorIndex = selectedRange.upperBound
+ let _highlightStartIndex: String.Index? = {
+ var index = text.index(before: cursorIndex)
+ while index > text.startIndex {
+ let char = text[index]
+ if char == "@" || char == "#" || char == ":" {
+ return index
+ }
+ index = text.index(before: index)
+ }
+ assert(index == text.startIndex)
+ let char = text[index]
+ if char == "@" || char == "#" || char == ":" {
+ return index
+ } else {
+ return nil
+ }
+ }()
+
+ guard let highlightStartIndex = _highlightStartIndex else { return nil }
+ let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil }
+ let symbolRange = highlightStartIndex.. Bool
+}
public final class ComposeContentViewModel: NSObject, ObservableObject {
@@ -28,12 +33,20 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// input
let context: AppContext
let kind: Kind
+ weak var delegate: ComposeContentViewModelDelegate?
@Published var viewLayoutFrame = ViewLayoutFrame()
// author (me)
@Published var authContext: AuthContext
+ // auto-complete info
+ @Published var autoCompleteRetryLayoutTimes = 0
+ @Published var autoCompleteInfo: AutoCompleteInfo? = nil
+
+ // emoji
+ var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource?
+
// output
// limit
@@ -42,10 +55,12 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// content
public weak var contentMetaText: MetaText? {
didSet {
-// guard let textView = contentMetaText?.textView else { return }
-// customEmojiPickerInputViewModel.configure(textInput: textView)
+ guard let textView = contentMetaText?.textView else { return }
+ customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
+ // for hashtag: "# "
+ // for mention: "@ "
@Published public var initialContent = ""
@Published public var content = ""
@Published public var contentWeightedLength = 0
@@ -56,8 +71,8 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// content warning
weak var contentWarningMetaText: MetaText? {
didSet {
- //guard let textView = contentWarningMetaText?.textView else { return }
- //customEmojiPickerInputViewModel.configure(textInput: textView)
+ guard let textView = contentWarningMetaText?.textView else { return }
+ customEmojiPickerInputViewModel.configure(textInput: textView)
}
}
@Published public var isContentWarningActive = false
@@ -76,7 +91,7 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// @Published public internal(set) var isMediaValid = true
// poll
- @Published var isPollActive = false
+ @Published public var isPollActive = false
@Published public var pollOptions: [PollComposeItem.Option] = {
// initial with 2 options
var options: [PollComposeItem.Option] = []
@@ -91,15 +106,26 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
// emoji
@Published var isEmojiActive = false
+ let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
+ let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
+ @Published var isLoadingCustomEmoji = false
// visibility
- @Published var visibility: Mastodon.Entity.Status.Visibility
+ @Published public var visibility: Mastodon.Entity.Status.Visibility
// UI & UX
@Published var replyToCellFrame: CGRect = .zero
@Published var contentCellFrame: CGRect = .zero
+ @Published var contentTextViewFrame: CGRect = .zero
@Published var scrollViewState: ScrollViewState = .fold
-
+
+ @Published var characterCount: Int = 0
+
+ @Published public private(set) var isPublishBarButtonItemEnabled = true
+ @Published var isAttachmentButtonEnabled = false
+ @Published var isPollButtonEnabled = false
+
+ @Published public private(set) var shouldDismiss = true
public init(
context: AppContext,
@@ -144,9 +170,102 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
}
return visibility
}()
+ self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(
+ for: authContext.mastodonAuthenticationBox.domain
+ )
super.init()
// end init
+ // setup initial value
+ switch kind {
+ case .reply(let record):
+ context.managedObjectContext.performAndWait {
+ guard let status = record.object(in: context.managedObjectContext) else {
+ assertionFailure()
+ return
+ }
+ let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
+
+ var mentionAccts: [String] = []
+ if author?.id != status.author.id {
+ mentionAccts.append("@" + status.author.acct)
+ }
+ let mentions = status.mentions
+ .filter { author?.id != $0.id }
+ for mention in mentions {
+ let acct = "@" + mention.acct
+ guard !mentionAccts.contains(acct) else { continue }
+ mentionAccts.append(acct)
+ }
+ for acct in mentionAccts {
+ UITextChecker.learnWord(acct)
+ }
+ if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
+ self.isContentWarningActive = true
+ self.contentWarning = spoilerText
+ }
+
+ let initialComposeContent = mentionAccts.joined(separator: " ")
+ let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
+ self.initialContent = preInsertedContent ?? ""
+ self.content = preInsertedContent ?? ""
+ }
+ case .hashtag(let hashtag):
+ let initialComposeContent = "#" + hashtag
+ UITextChecker.learnWord(initialComposeContent)
+ let preInsertedContent = initialComposeContent + " "
+ self.initialContent = preInsertedContent
+ self.content = preInsertedContent
+ case .mention(let record):
+ context.managedObjectContext.performAndWait {
+ guard let user = record.object(in: context.managedObjectContext) else { return }
+ let initialComposeContent = "@" + user.acct
+ UITextChecker.learnWord(initialComposeContent)
+ let preInsertedContent = initialComposeContent + " "
+ self.initialContent = preInsertedContent
+ self.content = preInsertedContent
+ }
+ case .post:
+ break
+ }
+
+ // set limit
+ let _configuration: Mastodon.Entity.Instance.Configuration? = {
+ var configuration: Mastodon.Entity.Instance.Configuration? = nil
+ context.managedObjectContext.performAndWait {
+ guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)
+ else { return }
+ configuration = authentication.instance?.configuration
+ }
+ return configuration
+ }()
+ if let configuration = _configuration {
+ // set character limit
+ if let maxCharacters = configuration.statuses?.maxCharacters {
+ maxTextInputLimit = maxCharacters
+ }
+ // set media limit
+ if let maxMediaAttachments = configuration.statuses?.maxMediaAttachments {
+ maxMediaAttachmentLimit = maxMediaAttachments
+ }
+ // set poll option limit
+ if let maxOptions = configuration.polls?.maxOptions {
+ maxPollOptionLimit = maxOptions
+ }
+ // TODO: more limit
+ }
+
+ bind()
+ }
+
+ deinit {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension ComposeContentViewModel {
+ private func bind() {
// bind author
$authContext
.sink { [weak self] authContext in
@@ -177,12 +296,138 @@ public final class ComposeContentViewModel: NSObject, ObservableObject {
)
.map { $0 + $1 <= $2 }
.assign(to: &$isContentValid)
- }
-
- deinit {
- os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- }
+
+ // bind attachment
+ $attachmentViewModels
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ Task {
+ try await self.uploadMediaInQueue()
+ }
+ }
+ .store(in: &disposeBag)
+
+ // bind emoji inputView
+ $isEmojiActive.assign(to: &customEmojiPickerInputViewModel.$isCustomEmojiComposing)
+
+ // bind toolbar
+ Publishers.CombineLatest3(
+ $isPollActive,
+ $attachmentViewModels,
+ $maxMediaAttachmentLimit
+ )
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] isPollActive, attachmentViewModels, maxMediaAttachmentLimit in
+ guard let self = self else { return }
+ let shouldMediaDisable = isPollActive || attachmentViewModels.count >= maxMediaAttachmentLimit
+ let shouldPollDisable = attachmentViewModels.count > 0
+
+ self.isAttachmentButtonEnabled = !shouldMediaDisable
+ self.isPollButtonEnabled = !shouldPollDisable
+ }
+ .store(in: &disposeBag)
+
+ // bind status content character count
+ Publishers.CombineLatest3(
+ $contentWeightedLength,
+ $contentWarningWeightedLength,
+ $isContentWarningActive
+ )
+ .map { contentWeightedLength, contentWarningWeightedLength, isContentWarningActive -> Int in
+ var count = contentWeightedLength
+ if isContentWarningActive {
+ count += contentWarningWeightedLength
+ }
+ return count
+ }
+ .assign(to: &$characterCount)
+
+ // bind compose bar button item UI state
+ let isComposeContentEmpty = $content
+ .map { $0.isEmpty }
+ let isComposeContentValid = Publishers.CombineLatest(
+ $characterCount,
+ $maxTextInputLimit
+ )
+ .map { characterCount, maxTextInputLimit in
+ characterCount <= maxTextInputLimit
+ }
+ let isMediaEmpty = $attachmentViewModels
+ .map { $0.isEmpty }
+ let isMediaUploadAllSuccess = $attachmentViewModels
+ .map { attachmentViewModels in
+ return Publishers.MergeMany(attachmentViewModels.map { $0.$uploadState })
+ .delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes
+ .map { _ in attachmentViewModels.map { $0.uploadState } }
+ }
+ .switchToLatest()
+ .map { outputs in
+ guard outputs.allSatisfy({ $0 == .finish }) else { return false }
+ return true
+ }
+
+ let isPollOptionsAllValid = $pollOptions
+ .map { options in
+ return Publishers.MergeMany(options.map { $0.$text })
+ .delay(for: 0.5, scheduler: DispatchQueue.main) // convert to outputs with delay. Due to @Published emit before changes
+ .map { _ in options.map { $0.text } }
+ }
+ .switchToLatest()
+ .map { outputs in
+ return outputs.allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
+ }
+
+ let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
+ isComposeContentEmpty,
+ isComposeContentValid,
+ isMediaEmpty,
+ isMediaUploadAllSuccess
+ )
+ .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
+ if isMediaEmpty {
+ return isComposeContentValid && !isComposeContentEmpty
+ } else {
+ return isComposeContentValid && isMediaUploadAllSuccess
+ }
+ }
+ .eraseToAnyPublisher()
+
+ let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
+ isComposeContentEmpty,
+ isComposeContentValid,
+ $isPollActive,
+ isPollOptionsAllValid
+ )
+ .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollOptionsAllValid -> Bool in
+ if isPollComposing {
+ return isComposeContentValid && !isComposeContentEmpty && isPollOptionsAllValid
+ } else {
+ return isComposeContentValid && !isComposeContentEmpty
+ }
+ }
+ .eraseToAnyPublisher()
+
+ Publishers.CombineLatest(
+ isPublishBarButtonItemEnabledPrecondition1,
+ isPublishBarButtonItemEnabledPrecondition2
+ )
+ .map { $0 && $1 }
+ .assign(to: &$isPublishBarButtonItemEnabled)
+
+ // bind modal dismiss state
+ $content
+ .receive(on: DispatchQueue.main)
+ .map { content in
+ if content.isEmpty {
+ return true
+ }
+ // if the trimmed content equal to initial content
+ return content.trimmingCharacters(in: .whitespacesAndNewlines) == self.initialContent
+ }
+ .assign(to: &$shouldDismiss)
+ }
}
extension ComposeContentViewModel {
@@ -192,13 +437,30 @@ extension ComposeContentViewModel {
case mention(user: ManagedObjectRecord)
case reply(status: ManagedObjectRecord)
}
-
+
public enum ScrollViewState {
case fold // snap to input
case expand // snap to reply
}
}
+extension ComposeContentViewModel {
+ public struct AutoCompleteInfo {
+ // model
+ let inputText: Substring
+ // range
+ let symbolRange: Range
+ let symbolString: Substring
+ let toCursorRange: Range
+ let toCursorString: Substring
+ let toHighlightEndRange: Range
+ let toHighlightEndString: Substring
+ // geometry
+ var textBoundingRect: CGRect = .zero
+ var symbolBoundingRect: CGRect = .zero
+ }
+}
+
extension ComposeContentViewModel {
func createNewPollOptionIfCould() {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
@@ -217,14 +479,14 @@ extension ComposeContentViewModel {
public var errorDescription: String? {
switch self {
case .pollHasEmptyOption:
- return "The post poll is invalid" // TODO: i18n
+ return L10n.Scene.Compose.Poll.thePollIsInvalid
}
}
public var failureReason: String? {
switch self {
case .pollHasEmptyOption:
- return "The poll has empty option" // TODO: i18n
+ return L10n.Scene.Compose.Poll.thePollHasEmptyOption
}
}
}
@@ -275,70 +537,58 @@ extension ComposeContentViewModel {
} // end func publisher()
}
-// MARK: - UITextViewDelegate
-extension ComposeContentViewModel: UITextViewDelegate {
- public func textViewDidBeginEditing(_ textView: UITextView) {
- switch textView {
- case contentMetaText?.textView:
- isContentEditing = true
- case contentWarningMetaText?.textView:
- isContentWarningEditing = true
- default:
- break
- }
- }
+extension ComposeContentViewModel {
- public func textViewDidEndEditing(_ textView: UITextView) {
- switch textView {
- case contentMetaText?.textView:
- isContentEditing = false
- case contentWarningMetaText?.textView:
- isContentWarningEditing = false
- default:
- break
+ public enum AttachmentPrecondition: Error, LocalizedError {
+ case videoAttachWithPhoto
+ case moreThanOneVideo
+
+ public var errorDescription: String? {
+ return L10n.Common.Alerts.PublishPostFailure.title
}
- }
-
- public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
- switch textView {
- case contentMetaText?.textView:
- return true
- case contentWarningMetaText?.textView:
- let isReturn = text == "\n"
- if isReturn {
- setContentTextViewFirstResponderIfNeeds()
+
+ public var failureReason: String? {
+ switch self {
+ case .videoAttachWithPhoto:
+ return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
+ case .moreThanOneVideo:
+ return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
+ }
+ }
+ }
+
+ // check exclusive limit:
+ // - up to 1 video
+ // - up to N photos
+ public func checkAttachmentPrecondition() throws {
+ let attachmentViewModels = self.attachmentViewModels
+ guard !attachmentViewModels.isEmpty else { return }
+
+ var photoAttachmentViewModels: [AttachmentViewModel] = []
+ var videoAttachmentViewModels: [AttachmentViewModel] = []
+ attachmentViewModels.forEach { attachmentViewModel in
+ guard let output = attachmentViewModel.output else {
+ assertionFailure()
+ return
+ }
+ switch output {
+ case .image:
+ photoAttachmentViewModels.append(attachmentViewModel)
+ case .video:
+ videoAttachmentViewModels.append(attachmentViewModel)
+ }
+ }
+
+ if !videoAttachmentViewModels.isEmpty {
+ guard videoAttachmentViewModels.count == 1 else {
+ throw AttachmentPrecondition.moreThanOneVideo
+ }
+ guard photoAttachmentViewModels.isEmpty else {
+ throw AttachmentPrecondition.videoAttachWithPhoto
}
- return !isReturn
- default:
- assertionFailure()
- return true
}
}
- func insertContentText(text: String) {
- guard let contentMetaText = self.contentMetaText else { return }
- // FIXME: smart prefix and suffix
- let string = contentMetaText.textStorage.string
- let isEmpty = string.isEmpty
- let hasPrefix = string.hasPrefix(" ")
- if hasPrefix || isEmpty {
- contentMetaText.textView.insertText(text)
- } else {
- contentMetaText.textView.insertText(" " + text)
- }
- }
-
- func setContentTextViewFirstResponderIfNeeds() {
- guard let contentMetaText = self.contentMetaText else { return }
- guard !contentMetaText.textView.isFirstResponder else { return }
- contentMetaText.textView.becomeFirstResponder()
- }
-
- func setContentWarningTextViewFirstResponderIfNeeds() {
- guard let contentWarningMetaText = self.contentWarningMetaText else { return }
- guard !contentWarningMetaText.textView.isFirstResponder else { return }
- contentWarningMetaText.textView.becomeFirstResponder()
- }
}
// MARK: - DeleteBackwardResponseTextFieldRelayDelegate
@@ -392,3 +642,56 @@ extension ComposeContentViewModel: DeleteBackwardResponseTextFieldRelayDelegate
}
}
+
+// MARK: - AttachmentViewModelDelegate
+extension ComposeContentViewModel: AttachmentViewModelDelegate {
+
+ public func attachmentViewModel(
+ _ viewModel: AttachmentViewModel,
+ uploadStateValueDidChange state: AttachmentViewModel.UploadState
+ ) {
+ Task {
+ try await uploadMediaInQueue()
+ }
+ }
+
+ @MainActor
+ func uploadMediaInQueue() async throws {
+ for (i, attachmentViewModel) in attachmentViewModels.enumerated() {
+ switch attachmentViewModel.uploadState {
+ case .none:
+ return
+ case .compressing:
+ return
+ case .ready:
+ let count = self.attachmentViewModels.count
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload \(i)/\(count) attachment")
+ try await attachmentViewModel.upload()
+ return
+ case .uploading:
+ return
+ case .fail:
+ return
+ case .finish:
+ continue
+ }
+ }
+ }
+
+ public func attachmentViewModel(
+ _ viewModel: AttachmentViewModel,
+ actionButtonDidPressed action: AttachmentViewModel.Action
+ ) {
+ switch action {
+ case .retry:
+ Task {
+ try await viewModel.upload(isRetry: true)
+ }
+ case .remove:
+ attachmentViewModels.removeAll(where: { $0 === viewModel })
+ Task {
+ try await uploadMediaInQueue()
+ }
+ }
+ }
+}
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerHeaderCollectionReusableView.swift
similarity index 100%
rename from Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerHeaderCollectionReusableView.swift
diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputView.swift
similarity index 100%
rename from Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputView.swift
diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift
similarity index 52%
rename from Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift
index 496c8191b..729524ce5 100644
--- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerInputViewModel.swift
@@ -9,7 +9,6 @@ import UIKit
import Combine
import MetaTextKit
import MastodonCore
-import MastodonUI
final class CustomEmojiPickerInputViewModel {
@@ -20,8 +19,7 @@ final class CustomEmojiPickerInputViewModel {
// input
weak var customEmojiPickerInputView: CustomEmojiPickerInputView?
- // output
- let isCustomEmojiComposing = CurrentValueSubject(false)
+ @Published var isCustomEmojiComposing = false
}
@@ -51,27 +49,28 @@ extension CustomEmojiPickerInputViewModel {
for reference in customEmojiReplaceableTextInputReferences {
guard let textInput = reference.value else { continue }
guard textInput.isFirstResponder == true else { continue }
- guard let selectedTextRange = textInput.selectedTextRange else { continue }
+ // guard let selectedTextRange = textInput.selectedTextRange else { continue }
textInput.insertText(text)
+ // FIXME: inline emoji
// due to insert text render as attachment
// the cursor reset logic not works
// hack with hard code +2 offset
- assert(text.hasSuffix(": "))
- guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue }
-
- if let _ = textInput as? MetaTextView {
- if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
- let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
- textInput.selectedTextRange = newSelectedTextRange
- }
- } else {
- if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) {
- let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
- textInput.selectedTextRange = newSelectedTextRange
- }
- }
+ // assert(text.hasSuffix(": "))
+ // guard text.hasPrefix(":") && text.hasSuffix(": ") else { continue }
+ //
+ // if let _ = textInput as? MetaTextView {
+ // if let newPosition = textInput.position(from: selectedTextRange.start, offset: 2) {
+ // let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
+ // textInput.selectedTextRange = newSelectedTextRange
+ // }
+ // } else {
+ // if let newPosition = textInput.position(from: selectedTextRange.start, offset: text.length) {
+ // let newSelectedTextRange = textInput.textRange(from: newPosition, to: newPosition)
+ // textInput.selectedTextRange = newSelectedTextRange
+ // }
+ // }
return reference
}
@@ -81,3 +80,16 @@ extension CustomEmojiPickerInputViewModel {
}
+extension CustomEmojiPickerInputViewModel {
+ public func configure(textInput: CustomEmojiReplaceableTextInput) {
+ $isCustomEmojiComposing
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] isCustomEmojiComposing in
+ guard let self = self else { return }
+ textInput.inputView = isCustomEmojiComposing ? self.customEmojiPickerInputView : nil
+ textInput.reloadInputViews()
+ self.append(customEmojiReplaceableTextInput: textInput)
+ }
+ .store(in: &disposeBag)
+ }
+}
diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerItemCollectionViewCell.swift
similarity index 100%
rename from Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/EmojiPicker/CustomEmojiPickerItemCollectionViewCell.swift
diff --git a/Mastodon/Helper/MastodonRegex.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift
similarity index 100%
rename from Mastodon/Helper/MastodonRegex.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Helper/MastodonRegex.swift
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift
index 5143bea35..fa409c114 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Poll/PollOptionTextField.swift
@@ -39,7 +39,7 @@ public struct PollOptionTextField: UIViewRepresentable {
textField.text = text
textField.placeholder = {
if index >= 0 {
- return L10n.Scene.Compose.Poll.optionNumber(index)
+ return L10n.Scene.Compose.Poll.optionNumber(index + 1)
} else {
assertionFailure()
return ""
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift
index ea3be18a8..93f3dd3a1 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift
@@ -119,13 +119,31 @@ extension MastodonStatusPublisher: StatusPublisher {
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
// upload media
do {
- let result = try await attachmentViewModel.upload(context: uploadContext)
- guard case let .mastodon(response) = result else {
- assertionFailure()
- continue
+ guard let attachment = attachmentViewModel.uploadResult else {
+ // precondition: all media uploaded
+ throw AppError.badRequest
}
- let attachmentID = response.value.id
- attachmentIDs.append(attachmentID)
+ attachmentIDs.append(attachment.id)
+
+ let caption = attachmentViewModel.caption
+ guard !caption.isEmpty else { continue }
+
+ _ = try await api.updateMedia(
+ domain: authContext.mastodonAuthenticationBox.domain,
+ attachmentID: attachment.id,
+ query: .init(
+ file: nil,
+ thumbnail: nil,
+ description: caption,
+ focus: nil
+ ),
+ mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
+ ).singleOutput()
+
+ // TODO: allow background upload
+ // let attachment = try await attachmentViewModel.upload(context: uploadContext)
+ // let attachmentID = attachment.id
+ // attachmentIDs.append(attachmentID)
} catch {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
_state = .failure(error)
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift
index 3a646f1fc..90d432825 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeContentTableViewCell.swift
@@ -7,74 +7,12 @@
import os.log
import UIKit
-import Combine
-import MetaTextKit
-import UITextView_Placeholder
-import MastodonAsset
-import MastodonLocalization
import UIHostingConfigurationBackport
-//protocol ComposeStatusContentTableViewCellDelegate: AnyObject {
-// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool
-//}
-
final class ComposeContentTableViewCell: UITableViewCell {
let logger = Logger(subsystem: "ComposeContentTableViewCell", category: "View")
-// var disposeBag = Set()
-// weak var delegate: ComposeStatusContentTableViewCellDelegate?
-//
-// let statusView = StatusView()
-//
-// let statusContentWarningEditorView = StatusContentWarningEditorView()
-//
-// let textEditorViewContainerView = UIView()
-//
-// static let metaTextViewTag: Int = 333
-// let metaText: MetaText = {
-// let metaText = MetaText()
-// metaText.textView.backgroundColor = .clear
-// metaText.textView.isScrollEnabled = false
-// metaText.textView.keyboardType = .twitter
-// metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment
-// metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset
-// metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
-// metaText.textView.attributedPlaceholder = {
-// var attributes = metaText.textAttributes
-// attributes[.foregroundColor] = Asset.Colors.Label.secondary.color
-// return NSAttributedString(
-// string: L10n.Scene.Compose.contentInputPlaceholder,
-// attributes: attributes
-// )
-// }()
-// metaText.paragraphStyle = {
-// let style = NSMutableParagraphStyle()
-// style.lineSpacing = 5
-// style.paragraphSpacing = 0
-// return style
-// }()
-// metaText.textAttributes = [
-// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)),
-// .foregroundColor: Asset.Colors.Label.primary.color,
-// ]
-// metaText.linkAttributes = [
-// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)),
-// .foregroundColor: Asset.Colors.brand.color,
-// ]
-// return metaText
-// }()
-//
-// // output
-// let contentWarningContent = PassthroughSubject()
-//
-// override func prepareForReuse() {
-// super.prepareForReuse()
-//
-// metaText.delegate = nil
-// metaText.textView.delegate = nil
-// }
-
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
_init()
@@ -93,79 +31,6 @@ extension ComposeContentTableViewCell {
selectionStyle = .none
layer.zPosition = 999
backgroundColor = .clear
-
-// let containerStackView = UIStackView()
-// containerStackView.axis = .vertical
-// containerStackView.translatesAutoresizingMaskIntoConstraints = false
-// contentView.addSubview(containerStackView)
-// NSLayoutConstraint.activate([
-// containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor),
-// containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-// containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
-// containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
-// ])
-// containerStackView.preservesSuperviewLayoutMargins = true
-//
-// containerStackView.addArrangedSubview(statusContentWarningEditorView)
-// statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical)
-//
-// let statusContainerView = UIView()
-// statusContainerView.preservesSuperviewLayoutMargins = true
-// containerStackView.addArrangedSubview(statusContainerView)
-// statusView.translatesAutoresizingMaskIntoConstraints = false
-// statusContainerView.addSubview(statusView)
-// NSLayoutConstraint.activate([
-// statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20),
-// statusView.leadingAnchor.constraint(equalTo: statusContainerView.leadingAnchor),
-// statusView.trailingAnchor.constraint(equalTo: statusContainerView.trailingAnchor),
-// statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor),
-// ])
-// statusView.setup(style: .composeStatusAuthor)
-//
-// containerStackView.addArrangedSubview(textEditorViewContainerView)
-// metaText.textView.translatesAutoresizingMaskIntoConstraints = false
-// textEditorViewContainerView.addSubview(metaText.textView)
-// NSLayoutConstraint.activate([
-// metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor),
-// metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor),
-// metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor),
-// metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor),
-// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh),
-// ])
-// statusContentWarningEditorView.textView.delegate = self
}
}
-
-// MARK: - UITextViewDelegate
-//extension ComposeStatusContentTableViewCell: UITextViewDelegate {
-//
-// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
-// return delegate?.composeStatusContentTableViewCell(self, textViewShouldBeginEditing: textView) ?? true
-// }
-//
-// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
-// switch textView {
-// case statusContentWarningEditorView.textView:
-// // disable input line break
-// guard text != "\n" else { return false }
-// return true
-// default:
-// assertionFailure()
-// return true
-// }
-// }
-//
-// func textViewDidChange(_ textView: UITextView) {
-// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): text: \(textView.text ?? "")")
-// guard textView === statusContentWarningEditorView.textView else { return }
-// // replace line break with space
-// // needs check input state to prevent break the IME
-// if textView.markedTextRange == nil {
-// textView.text = textView.text.replacingOccurrences(of: "\n", with: " ")
-// }
-// contentWarningContent.send(textView.text)
-// }
-//
-//}
-
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift
deleted file mode 100644
index 42a851bf1..000000000
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift
+++ /dev/null
@@ -1,172 +0,0 @@
-//
-// ComposeStatusAttachmentTableViewCell.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-6-29.
-//
-
-import UIKit
-import SwiftUI
-import Combine
-import AlamofireImage
-import MastodonAsset
-import MastodonCore
-import MastodonLocalization
-import UIHostingConfigurationBackport
-
-//final class ComposeStatusAttachmentTableViewCell: UITableViewCell {
-//
-// private(set) var dataSource: UICollectionViewDiffableDataSource!
-// weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
-// var observations = Set()
-//
-// private static func createLayout() -> UICollectionViewLayout {
-// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
-// let item = NSCollectionLayoutItem(layoutSize: itemSize)
-// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
-// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
-// let section = NSCollectionLayoutSection(group: group)
-// section.contentInsetsReference = .readableContent
-// return UICollectionViewCompositionalLayout(section: section)
-// }
-//
-// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
-// let collectionView: UICollectionView = {
-// let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout()
-// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
-// collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self))
-// collectionView.backgroundColor = .clear
-// collectionView.alwaysBounceVertical = true
-// collectionView.isScrollEnabled = false
-// return collectionView
-// }()
-// let collectionViewHeightDidUpdate = PassthroughSubject()
-//
-// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
-// super.init(style: style, reuseIdentifier: reuseIdentifier)
-// _init()
-// }
-//
-// required init?(coder: NSCoder) {
-// super.init(coder: coder)
-// _init()
-// }
-//
-//}
-//
-//extension ComposeStatusAttachmentTableViewCell {
-//
-// private func _init() {
-// backgroundColor = .clear
-// contentView.backgroundColor = .clear
-//
-// collectionView.translatesAutoresizingMaskIntoConstraints = false
-// contentView.addSubview(collectionView)
-// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh)
-// NSLayoutConstraint.activate([
-// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
-// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
-// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
-// collectionViewHeightLayoutConstraint,
-// ])
-//
-// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
-// guard let self = self else { return }
-// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
-// self.collectionViewHeightDidUpdate.send()
-// }
-// .store(in: &observations)
-//
-// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
-// [weak self] collectionView, indexPath, item -> UICollectionViewCell? in
-// guard let _ = self else { return UICollectionViewCell() }
-// switch item {
-// case .attachment:
-// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
-// cell.contentConfiguration = UIHostingConfigurationBackport {
-// HStack {
-// Image(systemName: "star")
-// Text("Favorites")
-// Spacer()
-// }
-// }
-//// cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value
-//// cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate
-//// attachmentService.thumbnailImage
-//// .receive(on: DispatchQueue.main)
-//// .sink { [weak cell] thumbnailImage in
-//// guard let cell = cell else { return }
-//// let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1)
-//// guard let image = thumbnailImage else {
-//// let placeholder = UIImage.placeholder(
-//// size: size,
-//// color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
-//// )
-//// .af.imageRounded(
-//// withCornerRadius: AttachmentContainerView.containerViewCornerRadius
-//// )
-//// cell.attachmentContainerView.previewImageView.image = placeholder
-//// return
-//// }
-//// // cannot get correct size. set corner radius on layer
-//// cell.attachmentContainerView.previewImageView.image = image
-//// }
-//// .store(in: &cell.disposeBag)
-//// Publishers.CombineLatest(
-//// attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(),
-//// attachmentService.error.eraseToAnyPublisher()
-//// )
-//// .receive(on: DispatchQueue.main)
-//// .sink { [weak cell, weak attachmentService] uploadState, error in
-//// guard let cell = cell else { return }
-//// guard let attachmentService = attachmentService else { return }
-//// cell.attachmentContainerView.emptyStateView.isHidden = error == nil
-//// cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil
-//// if let error = error {
-//// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
-//// cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription
-//// } else {
-//// guard let uploadState = uploadState else { return }
-//// switch uploadState {
-//// case is MastodonAttachmentService.UploadState.Finish:
-//// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
-//// case is MastodonAttachmentService.UploadState.Fail:
-//// cell.attachmentContainerView.activityIndicatorView.stopAnimating()
-//// // FIXME: not display
-//// cell.attachmentContainerView.emptyStateView.label.text = {
-//// if let file = attachmentService.file.value {
-//// switch file {
-//// case .jpeg, .png, .gif:
-//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
-//// case .other:
-//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
-//// }
-//// } else {
-//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
-//// }
-//// }()
-//// default:
-//// break
-//// }
-//// }
-//// }
-//// .store(in: &cell.disposeBag)
-//// NotificationCenter.default.publisher(
-//// for: UITextView.textDidChangeNotification,
-//// object: cell.attachmentContainerView.descriptionTextView
-//// )
-//// .receive(on: DispatchQueue.main)
-//// .sink { notification in
-//// guard let textField = notification.object as? UITextView else { return }
-//// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
-//// attachmentService.description.value = text
-//// }
-//// .store(in: &cell.disposeBag)
-// return cell
-// }
-// }
-// }
-//
-//}
-//
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift
deleted file mode 100644
index 27b835a5a..000000000
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift
+++ /dev/null
@@ -1,209 +0,0 @@
-//
-// ComposeStatusPollTableViewCell.swift
-// Mastodon
-//
-// Created by MainasuK Cirno on 2021-6-29.
-//
-
-import os.log
-import UIKit
-import Combine
-import MastodonAsset
-import MastodonLocalization
-
-//protocol ComposeStatusPollTableViewCellDelegate: AnyObject {
-// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute])
-//}
-//
-//final class ComposeStatusPollTableViewCell: UITableViewCell {
-//
-// let logger = Logger(subsystem: "ComposeStatusPollTableViewCell", category: "UI")
-//
-// private(set) var dataSource: UICollectionViewDiffableDataSource!
-// var observations = Set()
-//
-// weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel?
-// weak var delegate: ComposeStatusPollTableViewCellDelegate?
-// weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate?
-// weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
-// weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate?
-//
-// private static func createLayout() -> UICollectionViewLayout {
-// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
-// let item = NSCollectionLayoutItem(layoutSize: itemSize)
-// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
-// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
-// let section = NSCollectionLayoutSection(group: group)
-// section.contentInsetsReference = .readableContent
-// return UICollectionViewCompositionalLayout(section: section)
-// }
-//
-// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint!
-// let collectionView: UICollectionView = {
-// let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout()
-// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
-// collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self))
-// collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self))
-// collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self))
-// collectionView.backgroundColor = .clear
-// collectionView.alwaysBounceVertical = true
-// collectionView.isScrollEnabled = false
-// collectionView.dragInteractionEnabled = true
-// return collectionView
-// }()
-// let collectionViewHeightDidUpdate = PassthroughSubject()
-//
-// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
-// super.init(style: style, reuseIdentifier: reuseIdentifier)
-// _init()
-// }
-//
-// required init?(coder: NSCoder) {
-// super.init(coder: coder)
-// _init()
-// }
-//
-//}
-//
-//extension ComposeStatusPollTableViewCell {
-//
-// private func _init() {
-// backgroundColor = .clear
-// contentView.backgroundColor = .clear
-//
-// collectionView.translatesAutoresizingMaskIntoConstraints = false
-// contentView.addSubview(collectionView)
-// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh)
-// NSLayoutConstraint.activate([
-// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
-// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
-// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
-// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
-// collectionViewHeightLayoutConstraint,
-// ])
-//
-// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in
-// guard let self = self else { return }
-// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height
-// self.collectionViewHeightDidUpdate.send()
-// }
-// .store(in: &observations)
-//
-// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [
-// weak self
-// ] collectionView, indexPath, item -> UICollectionViewCell? in
-// guard let self = self else { return UICollectionViewCell() }
-//
-// switch item {
-// case .pollOption(let attribute):
-// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell
-// cell.pollOptionView.optionTextField.text = attribute.option.value
-// cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1)
-// cell.pollOption
-// .receive(on: DispatchQueue.main)
-// .assign(to: \.value, on: attribute.option)
-// .store(in: &cell.disposeBag)
-// cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate
-// if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel {
-// ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
-// }
-// return cell
-// case .pollOptionAppendEntry:
-// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
-// cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate
-// return cell
-// case .pollExpiresOption(let attribute):
-// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell
-// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal)
-// attribute.expiresOption
-// .receive(on: DispatchQueue.main)
-// .sink { [weak cell] expiresOption in
-// guard let cell = cell else { return }
-// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal)
-// }
-// .store(in: &cell.disposeBag)
-// cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate
-// return cell
-// }
-// }
-//
-// collectionView.dragDelegate = self
-// collectionView.dropDelegate = self
-// }
-//
-//}
-//
-//// MARK: - UICollectionViewDragDelegate
-//extension ComposeStatusPollTableViewCell: UICollectionViewDragDelegate {
-//
-// func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
-// guard let item = dataSource.itemIdentifier(for: indexPath) else { return [] }
-// switch item {
-// case .pollOption:
-// let itemProvider = NSItemProvider(object: String(item.hashValue) as NSString)
-// let dragItem = UIDragItem(itemProvider: itemProvider)
-// dragItem.localObject = item
-// return [dragItem]
-// default:
-// return []
-// }
-// }
-//
-// func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool {
-// // drag to app should be the same app
-// return true
-// }
-//}
-//
-//// MARK: - UICollectionViewDropDelegate
-//extension ComposeStatusPollTableViewCell: UICollectionViewDropDelegate {
-// // didUpdate
-// func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
-// guard collectionView.hasActiveDrag,
-// let destinationIndexPath = destinationIndexPath,
-// let item = dataSource.itemIdentifier(for: destinationIndexPath)
-// else {
-// return UICollectionViewDropProposal(operation: .forbidden)
-// }
-//
-// switch item {
-// case .pollOption:
-// return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
-// default:
-// return UICollectionViewDropProposal(operation: .cancel)
-// }
-// }
-//
-// // performDrop
-// func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
-// guard let dropItem = coordinator.items.first,
-// let item = dropItem.dragItem.localObject as? ComposeStatusPollItem,
-// case .pollOption = item
-// else { return }
-//
-// guard coordinator.proposal.operation == .move else { return }
-// guard let destinationIndexPath = coordinator.destinationIndexPath,
-// let _ = collectionView.cellForItem(at: destinationIndexPath) as? ComposeStatusPollOptionCollectionViewCell
-// else { return }
-//
-// var snapshot = dataSource.snapshot()
-// guard destinationIndexPath.row < snapshot.itemIdentifiers.count else { return }
-// let anchorItem = snapshot.itemIdentifiers[destinationIndexPath.row]
-// snapshot.moveItem(item, afterItem: anchorItem)
-// dataSource.apply(snapshot)
-//
-// coordinator.drop(dropItem.dragItem, toItemAt: destinationIndexPath)
-// }
-//}
-//
-//extension ComposeStatusPollTableViewCell: UICollectionViewDelegate {
-// func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
-// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(originalIndexPath.debugDescription) -> \(proposedIndexPath.debugDescription)")
-//
-// guard let _ = collectionView.cellForItem(at: proposedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else {
-// return originalIndexPath
-// }
-//
-// return proposedIndexPath
-// }
-//}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift
similarity index 83%
rename from MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift
index 4a34c77d4..6374203fa 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView+ViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView+ViewModel.swift
@@ -27,6 +27,9 @@ extension ComposeContentToolbarView {
@Published var isEmojiActive = false
@Published var isContentWarningActive = false
+ @Published var isAttachmentButtonEnabled = false
+ @Published var isPollButtonEnabled = false
+
@Published public var maxTextInputLimit = 500
@Published public var contentWeightedLength = 0
@Published public var contentWarningWeightedLength = 0
@@ -120,4 +123,19 @@ extension ComposeContentToolbarView.ViewModel {
return action.inactiveImage
}
}
+
+ func label(for action: Action) -> String {
+ switch action {
+ case .attachment:
+ return L10n.Scene.Compose.Accessibility.appendAttachment
+ case .poll:
+ return isPollActive ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
+ case .emoji:
+ return L10n.Scene.Compose.Accessibility.customEmojiPicker
+ case .contentWarning:
+ return isContentWarningActive ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
+ case .visibility:
+ return L10n.Scene.Compose.Accessibility.postVisibilityMenu
+ }
+ }
}
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift
similarity index 82%
rename from MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift
rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift
index 52026c636..683164bdd 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentToolbarView.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.swift
@@ -44,7 +44,9 @@ struct ComposeContentToolbarView: View {
}
} label: {
label(for: action)
+ .opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5)
}
+ .disabled(!viewModel.isAttachmentButtonEnabled)
.frame(width: 48, height: 48)
case .visibility:
Menu {
@@ -61,8 +63,19 @@ struct ComposeContentToolbarView: View {
}
} label: {
label(for: viewModel.visibility.image)
+ .accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
}
.frame(width: 48, height: 48)
+ case .poll:
+ Button {
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")
+ viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action)
+ } label: {
+ label(for: action)
+ .opacity(viewModel.isPollButtonEnabled ? 1.0 : 0.5)
+ }
+ .disabled(!viewModel.isPollButtonEnabled)
+ .frame(width: 48, height: 48)
default:
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")
@@ -86,11 +99,14 @@ struct ComposeContentToolbarView: View {
Text("\(remains)")
.foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel))
.font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular))
+ .accessibilityLabel(L10n.A11y.Plural.Count.charactersLeft(remains))
}
.padding(.leading, 4) // 4 + 12 = 16
.padding(.trailing, 16)
.frame(height: ComposeContentToolbarView.toolbarHeight)
.background(Color(viewModel.backgroundColor))
+ .accessibilityElement(children: .contain)
+ .accessibilityLabel(L10n.Scene.Compose.Accessibility.postOptions)
}
}
@@ -100,6 +116,7 @@ extension ComposeContentToolbarView {
Image(uiImage: viewModel.image(for: action))
.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color))
.frame(width: 24, height: 24, alignment: .center)
+ .accessibilityLabel(viewModel.label(for: action))
}
func label(for image: UIImage) -> some View {
diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift
index 25584848a..10ca4e71f 100644
--- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift
+++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift
@@ -11,15 +11,18 @@ import MastodonAsset
import MastodonCore
import MastodonLocalization
import Stripes
+import Kingfisher
public struct ComposeContentView: View {
static let logger = Logger(subsystem: "ComposeContentView", category: "View")
var logger: Logger { ComposeContentView.logger }
+ static let contentViewCoordinateSpace = "ComposeContentView.Content"
static var margin: CGFloat = 16
@ObservedObject var viewModel: ComposeContentViewModel
+
public var body: some View {
VStack(spacing: .zero) {
@@ -105,9 +108,25 @@ public struct ComposeContentView: View {
.frame(minHeight: 100)
.fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, ComposeContentView.margin)
+ .background(
+ GeometryReader { proxy in
+ Color.clear.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .named(ComposeContentView.contentViewCoordinateSpace)))
+ }
+ .onPreferenceChange(ViewFramePreferenceKey.self) { frame in
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): content textView frame: \(frame.debugDescription)")
+ let rect = frame.standardized
+ viewModel.contentTextViewFrame = CGRect(
+ origin: frame.origin,
+ size: CGSize(width: floor(rect.width), height: floor(rect.height))
+ )
+ }
+ )
// poll
pollView
.padding(.horizontal, ComposeContentView.margin)
+ // media
+ mediaView
+ .padding(.horizontal, ComposeContentView.margin)
}
.background(
GeometryReader { proxy in
@@ -124,6 +143,7 @@ public struct ComposeContentView: View {
)
Spacer()
} // end VStack
+ .coordinateSpace(name: ComposeContentView.contentViewCoordinateSpace)
} // end body
}
@@ -147,6 +167,8 @@ extension ComposeContentView {
}
Spacer()
}
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(L10n.Scene.Compose.Accessibility.postingAs([viewModel.name.string, viewModel.username].joined(separator: ", ")))
}
}
@@ -165,7 +187,7 @@ extension ComposeContentView {
index: _index,
deleteBackwardResponseTextFieldRelayDelegate: viewModel
) { textField in
- // viewModel.customEmojiPickerInputViewModel.configure(textInput: textField)
+ viewModel.customEmojiPickerInputViewModel.configure(textInput: textField)
}
}
if viewModel.maxPollOptionLimit != viewModel.pollOptions.count {
@@ -194,6 +216,24 @@ extension ComposeContentView {
}
} // end VStack
}
+
+ // MARK: - media
+ var mediaView: some View {
+ VStack(spacing: 16) {
+ ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in
+ AttachmentView(viewModel: attachmentViewModel)
+ .clipShape(Rectangle())
+ .badgeView(
+ Button {
+ viewModel.attachmentViewModels.removeAll(where: { $0 === attachmentViewModel })
+ } label: {
+ Image(systemName: "minus.circle.fill")
+ .foregroundColor(.red)
+ }
+ )
+ } // end ForEach
+ } // end VStack
+ }
}
//private struct ScrollOffsetPreferenceKey: PreferenceKey {
diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift b/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift
new file mode 100644
index 000000000..f9b09e740
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Vendor/CircleProgressView.swift
@@ -0,0 +1,29 @@
+//
+// CircleProgressView.swift
+//
+//
+// Created by MainasuK on 2022/11/10.
+//
+
+import Foundation
+import SwiftUI
+
+/// https://stackoverflow.com/a/71467536/3797903
+struct CircleProgressView: View {
+
+ let progress: Double
+
+ var body: some View {
+ let lineWidth: CGFloat = 4
+ let tintColor = Color.white
+ ZStack {
+ Circle()
+ .trim(from: 0.0, to: CGFloat(progress))
+ .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .butt, lineJoin: .bevel))
+ .foregroundColor(tintColor)
+ .rotationEffect(Angle(degrees: 270.0))
+ }
+ .padding(ceil(lineWidth / 2))
+ }
+
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/MetaTextView+PasteExtensions.swift b/MastodonSDK/Sources/MastodonUI/Vendor/MetaTextView+PasteExtensions.swift
new file mode 100644
index 000000000..8fe1949af
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Vendor/MetaTextView+PasteExtensions.swift
@@ -0,0 +1,29 @@
+//
+// MetaTextView+PasteExtensions.swift
+// Mastodon
+//
+// Created by Rick Kerkhof on 30/10/2022.
+//
+
+import Foundation
+import MetaTextKit
+import UIKit
+
+extension MetaTextView {
+ public override func paste(_ sender: Any?) {
+ super.paste(sender)
+
+ var nextResponder = self.next;
+
+ // Force the event to bubble through ALL responders
+ // This is a workaround as somewhere down the chain the paste event gets eaten
+ while (nextResponder != nil) {
+ if let nextResponder = nextResponder {
+ if (nextResponder.responds(to: #selector(UIResponderStandardEditActions.paste(_:)))) {
+ nextResponder.perform(#selector(UIResponderStandardEditActions.paste(_:)), with: sender)
+ }
+ }
+ nextResponder = nextResponder?.next;
+ }
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift b/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift
new file mode 100644
index 000000000..fe89b0457
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift
@@ -0,0 +1,15 @@
+//
+// VisualEffectView.swift
+//
+//
+// Created by MainasuK on 2022/11/8.
+//
+
+import SwiftUI
+
+// ref: https://stackoverflow.com/a/59111492/3797903
+public struct VisualEffectView: UIViewRepresentable {
+ public var effect: UIVisualEffect?
+ public func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() }
+ public func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect }
+}
diff --git a/Podfile b/Podfile
index 28757d528..4df2d4d7c 100644
--- a/Podfile
+++ b/Podfile
@@ -8,11 +8,10 @@ target 'Mastodon' do
# Pods for Mastodon
# UI
- pod 'UITextField+Shake', '~> 1.2'
pod 'XLPagerTabStrip', '~> 9.0.0'
# misc
- pod 'SwiftGen', '~> 6.4.0'
+ pod 'SwiftGen', '~> 6.6.2'
pod 'DateToolsSwift', '~> 5.0.0'
pod 'Kanna', '~> 5.2.2'
pod 'Sourcery', '~> 1.6.1'
diff --git a/Podfile.lock b/Podfile.lock
index 12680db21..c7220b00a 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -5,8 +5,7 @@ PODS:
- Sourcery (1.6.1):
- Sourcery/CLI-Only (= 1.6.1)
- Sourcery/CLI-Only (1.6.1)
- - SwiftGen (6.4.0)
- - "UITextField+Shake (1.2.1)"
+ - SwiftGen (6.6.2)
- XLPagerTabStrip (9.0.0)
DEPENDENCIES:
@@ -14,8 +13,7 @@ DEPENDENCIES:
- FLEX (~> 4.4.0)
- Kanna (~> 5.2.2)
- Sourcery (~> 1.6.1)
- - SwiftGen (~> 6.4.0)
- - "UITextField+Shake (~> 1.2)"
+ - SwiftGen (~> 6.6.2)
- XLPagerTabStrip (~> 9.0.0)
SPEC REPOS:
@@ -25,7 +23,6 @@ SPEC REPOS:
- Kanna
- Sourcery
- SwiftGen
- - "UITextField+Shake"
- XLPagerTabStrip
SPEC CHECKSUMS:
@@ -33,10 +30,9 @@ SPEC CHECKSUMS:
FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab
Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
- SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
- "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
+ SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
-PODFILE CHECKSUM: 8b15fb6d4e801b7a7e7761a2e2fe40a89b1da4ff
+PODFILE CHECKSUM: 7499a197793f73c4dcf1d16a315434baaa688873
COCOAPODS: 1.11.3
diff --git a/README.md b/README.md
index bf35b4599..d2a19e715 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# Mastodon
+
[![CI](https://github.com/mastodon/mastodon-ios/actions/workflows/main.yml/badge.svg)](https://github.com/mastodon/mastodon-ios/actions/workflows/main.yml) [![Crowdin](https://badges.crowdin.net/mastodon-for-ios/localized.svg)](https://crowdin.com/project/mastodon-for-ios)
@@ -11,12 +12,14 @@ This is the repository for the official iOS App for Mastodon. You can install it
Read this blog post for this app to learn more.
> [Developing an official iOS app for Mastodon](https://blog.joinmastodon.org/2021/02/developing-an-official-ios-app-for-mastodon/)
-## Getting Start
+## Getting Started
+
- Read the setup guide [here](./Documentation/Setup.md)
- About [contributing](./Documentation/CONTRIBUTING.md)
- [Documentation folder](./Documentation/)
## Acknowledgments
+
Thanks to these open-sources projects listed [here](./Documentation/Acknowledgments.md).
## License
diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift
deleted file mode 100644
index c93c05147..000000000
--- a/ShareActionExtension/Scene/ComposeViewController.swift
+++ /dev/null
@@ -1,327 +0,0 @@
-//
-// ComposeViewController.swift
-// MastodonShareAction
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import os.log
-import UIKit
-import Combine
-import MastodonUI
-import SwiftUI
-import MastodonAsset
-import MastodonLocalization
-import MastodonCore
-import MastodonUI
-
-class ComposeViewController: UIViewController {
-
- let logger = Logger(subsystem: "ComposeViewController", category: "ViewController")
-
- let context = AppContext()
-
- var disposeBag = Set()
- private(set) lazy var viewModel = ComposeViewModel(context: context)
-
- let publishButton: UIButton = {
- let button = RoundedEdgesButton(type: .custom)
- button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
- button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
- button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color), for: .normal)
- button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color.withAlphaComponent(0.5)), for: .highlighted)
- button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
- button.setTitleColor(.white, for: .normal)
- button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
- button.adjustsImageWhenHighlighted = false
- return button
- }()
-
- private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
- private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
- let barButtonItem = UIBarButtonItem(customView: publishButton)
- publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
- return barButtonItem
- }()
-
- let activityIndicatorBarButtonItem: UIBarButtonItem = {
- let indicatorView = UIActivityIndicatorView(style: .medium)
- let barButtonItem = UIBarButtonItem(customView: indicatorView)
- indicatorView.startAnimating()
- return barButtonItem
- }()
-
-
-// let viewSafeAreaDidChange = PassthroughSubject()
-// let composeToolbarView = ComposeToolbarView()
-// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
-// let composeToolbarBackgroundView = UIView()
-}
-
-extension ComposeViewController {
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
-// navigationController?.presentationController?.delegate = self
-//
-// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
-// ThemeService.shared.currentTheme
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] theme in
-// guard let self = self else { return }
-// self.setupBackgroundColor(theme: theme)
-// }
-// .store(in: &disposeBag)
-//
-// navigationItem.leftBarButtonItem = cancelBarButtonItem
-// viewModel.isBusy
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] isBusy in
-// guard let self = self else { return }
-// self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem
-// }
-// .store(in: &disposeBag)
-//
-// let hostingViewController = UIHostingController(
-// rootView: ComposeView().environmentObject(viewModel.composeViewModel)
-// )
-// addChild(hostingViewController)
-// view.addSubview(hostingViewController.view)
-// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
-// view.addSubview(hostingViewController.view)
-// NSLayoutConstraint.activate([
-// hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
-// hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
-// hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
-// hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
-// ])
-// hostingViewController.didMove(toParent: self)
-//
-// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
-// view.addSubview(composeToolbarView)
-// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
-// NSLayoutConstraint.activate([
-// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
-// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
-// composeToolbarViewBottomLayoutConstraint,
-// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
-// ])
-// composeToolbarView.preservesSuperviewLayoutMargins = true
-// composeToolbarView.delegate = self
-//
-// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
-// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
-// NSLayoutConstraint.activate([
-// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
-// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
-// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
-// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
-// ])
-//
-// // FIXME: using iOS 15 toolbar for .keyboard placement
-// let keyboardEventPublishers = Publishers.CombineLatest3(
-// KeyboardResponderService.shared.isShow,
-// KeyboardResponderService.shared.state,
-// KeyboardResponderService.shared.endFrame
-// )
-//
-// Publishers.CombineLatest(
-// keyboardEventPublishers,
-// viewSafeAreaDidChange
-// )
-// .sink(receiveValue: { [weak self] keyboardEvents, _ in
-// guard let self = self else { return }
-//
-// let (isShow, state, endFrame) = keyboardEvents
-// guard isShow, state == .dock else {
-// UIView.animate(withDuration: 0.3) {
-// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
-// self.view.layoutIfNeeded()
-// }
-// return
-// }
-// // isShow AND dock state
-//
-// UIView.animate(withDuration: 0.3) {
-// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
-// self.view.layoutIfNeeded()
-// }
-// })
-// .store(in: &disposeBag)
-//
-// // bind visibility toolbar UI
-// Publishers.CombineLatest(
-// viewModel.selectedStatusVisibility,
-// viewModel.traitCollectionDidChangePublisher
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] type, _ in
-// guard let self = self else { return }
-// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
-// self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
-// self.composeToolbarView.activeVisibilityType.value = type
-// }
-// .store(in: &disposeBag)
-//
-// // bind counter
-// viewModel.characterCount
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] characterCount in
-// guard let self = self else { return }
-// let count = ShareViewModel.composeContentLimit - characterCount
-// self.composeToolbarView.characterCountLabel.text = "\(count)"
-// switch count {
-// case _ where count < 0:
-// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
-// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color
-// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count))
-// default:
-// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
-// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color
-// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count)
-// }
-// }
-// .store(in: &disposeBag)
-//
-// // bind valid
-// viewModel.isValid
-// .receive(on: DispatchQueue.main)
-// .assign(to: \.isEnabled, on: publishButton)
-// .store(in: &disposeBag)
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
-
-// viewModel.viewDidAppear.value = true
-// viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
-//
-// viewModel.composeViewModel.viewDidAppear = true
- }
-
- override func viewSafeAreaInsetsDidChange() {
- super.viewSafeAreaInsetsDidChange()
-
-// viewSafeAreaDidChange.send()
- }
-
- override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- super.traitCollectionDidChange(previousTraitCollection)
-
-// viewModel.traitCollectionDidChangePublisher.send()
- }
-
-}
-
-//extension ComposeViewController {
-// private func setupBackgroundColor(theme: Theme) {
-// view.backgroundColor = theme.systemElevatedBackgroundColor
-// viewModel.composeViewModel.backgroundColor = theme.systemElevatedBackgroundColor
-// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor
-//
-// let barAppearance = UINavigationBarAppearance()
-// barAppearance.configureWithDefaultBackground()
-// barAppearance.backgroundColor = theme.navigationBarBackgroundColor
-// navigationItem.standardAppearance = barAppearance
-// navigationItem.compactAppearance = barAppearance
-// navigationItem.scrollEdgeAppearance = barAppearance
-// }
-//
-// private func showDismissConfirmAlertController() {
-// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension
-// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in
-// self.extensionContext?.cancelRequest(withError: ShareViewModel.ShareError.userCancelShare)
-// }
-// alertController.addAction(discardAction)
-// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil)
-// alertController.addAction(okAction)
-// self.present(alertController, animated: true, completion: nil)
-// }
-//}
-//
-extension ComposeViewController {
- @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
- logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-
-// showDismissConfirmAlertController()
- }
-
- @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
- logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-
-// viewModel.isPublishing.value = true
-//
-// viewModel.publish()
-// .delay(for: 2, scheduler: DispatchQueue.main)
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] completion in
-// guard let self = self else { return }
-// self.viewModel.isPublishing.value = false
-//
-// switch completion {
-// case .failure:
-// let alertController = UIAlertController(
-// title: L10n.Common.Alerts.PublishPostFailure.title,
-// message: L10n.Common.Alerts.PublishPostFailure.message,
-// preferredStyle: .actionSheet // can not use alert in extension
-// )
-// let okAction = UIAlertAction(
-// title: L10n.Common.Controls.Actions.ok,
-// style: .cancel,
-// handler: nil
-// )
-// alertController.addAction(okAction)
-// self.present(alertController, animated: true, completion: nil)
-// case .finished:
-// self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal)
-// self.publishButton.isUserInteractionEnabled = false
-// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
-// guard let self = self else { return }
-// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
-// }
-// }
-// } receiveValue: { response in
-// // do nothing
-// }
-// .store(in: &disposeBag)
- }
-}
-
-//// MARK - ComposeToolbarViewDelegate
-//extension ComposeViewController: ComposeToolbarViewDelegate {
-//
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
-// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-//
-// withAnimation {
-// viewModel.composeViewModel.isContentWarningComposing.toggle()
-// }
-// }
-//
-// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
-// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-//
-// viewModel.selectedStatusVisibility.value = type
-// }
-//
-//}
-//
-//// MARK: - UIAdaptivePresentationControllerDelegate
-//extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
-//
-// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
-// return viewModel.shouldDismiss.value
-// }
-//
-// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-// showDismissConfirmAlertController()
-//
-// }
-//
-// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
-// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
-// }
-//
-//}
diff --git a/ShareActionExtension/Scene/ComposeViewModel.swift b/ShareActionExtension/Scene/ComposeViewModel.swift
deleted file mode 100644
index 4470cfbe8..000000000
--- a/ShareActionExtension/Scene/ComposeViewModel.swift
+++ /dev/null
@@ -1,417 +0,0 @@
-//
-// ComposeViewModel.swift
-// MastodonShareAction
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import os.log
-import Foundation
-import Combine
-import CoreData
-import CoreDataStack
-import MastodonSDK
-import MastodonUI
-import SwiftUI
-import UniformTypeIdentifiers
-import MastodonAsset
-import MastodonLocalization
-import MastodonUI
-import MastodonCore
-
-final class ComposeViewModel {
-
- let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
-
- var disposeBag = Set()
-
- static let composeContentLimit: Int = 500
-
- // input
- let context: AppContext
-
-// private var coreDataStack: CoreDataStack?
-// var managedObjectContext: NSManagedObjectContext?
-// var api: APIService?
-//
-// var inputItems = CurrentValueSubject<[NSExtensionItem], Never>([])
-// let viewDidAppear = CurrentValueSubject(false)
-// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit
-// let selectedStatusVisibility = CurrentValueSubject(.public)
-//
-// // output
-// let authentication = CurrentValueSubject?, Never>(nil)
-// let isFetchAuthentication = CurrentValueSubject(true)
-// let isPublishing = CurrentValueSubject(false)
-// let isBusy = CurrentValueSubject(true)
-// let isValid = CurrentValueSubject(false)
-// let shouldDismiss = CurrentValueSubject(true)
-// let composeViewModel = ComposeViewModel()
-// let characterCount = CurrentValueSubject(0)
-
- init(context: AppContext) {
- self.context = context
- // end init
-
-// viewDidAppear.receive(on: DispatchQueue.main)
-// .removeDuplicates()
-// .sink { [weak self] viewDidAppear in
-// guard let self = self else { return }
-// guard viewDidAppear else { return }
-// self.setupCoreData()
-// }
-// .store(in: &disposeBag)
-//
-// Publishers.CombineLatest(
-// inputItems.removeDuplicates(),
-// viewDidAppear.removeDuplicates()
-// )
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] inputItems, _ in
-// guard let self = self else { return }
-// self.parse(inputItems: inputItems)
-// }
-// .store(in: &disposeBag)
-//
-// // bind authentication loading state
-// authentication
-// .map { result in result == nil }
-// .assign(to: \.value, on: isFetchAuthentication)
-// .store(in: &disposeBag)
-//
-// // bind user locked state
-// authentication
-// .compactMap { result -> Bool? in
-// guard let result = result else { return nil }
-// switch result {
-// case .success(let authentication):
-// return authentication.user.locked
-// case .failure:
-// return nil
-// }
-// }
-// .map { locked -> ComposeToolbarView.VisibilitySelectionType in
-// locked ? .private : .public
-// }
-// .assign(to: \.value, on: selectedStatusVisibility)
-// .store(in: &disposeBag)
-//
-// // bind author
-// authentication
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] result in
-// guard let self = self else { return }
-// guard let result = result else { return }
-// switch result {
-// case .success(let authentication):
-// self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL()
-// self.composeViewModel.authorName = authentication.user.displayNameWithFallback
-// self.composeViewModel.authorUsername = "@" + authentication.user.username
-// case .failure:
-// self.composeViewModel.avatarImageURL = nil
-// self.composeViewModel.authorName = " "
-// self.composeViewModel.authorUsername = " "
-// }
-// }
-// .store(in: &disposeBag)
-//
-// // bind authentication to compose view model
-// authentication
-// .map { result -> MastodonAuthentication? in
-// guard let result = result else { return nil }
-// switch result {
-// case .success(let authentication):
-// return authentication
-// case .failure:
-// return nil
-// }
-// }
-// .assign(to: &composeViewModel.$authentication)
-//
-// // bind isBusy
-// Publishers.CombineLatest(
-// isFetchAuthentication,
-// isPublishing
-// )
-// .receive(on: DispatchQueue.main)
-// .map { $0 || $1 }
-// .assign(to: \.value, on: isBusy)
-// .store(in: &disposeBag)
-//
-// // pass initial i18n string
-// composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder
-// composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder
-// composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight
-//
-// // bind compose bar button item UI state
-// let isComposeContentEmpty = composeViewModel.$statusContent
-// .map { $0.isEmpty }
-//
-// isComposeContentEmpty
-// .assign(to: \.value, on: shouldDismiss)
-// .store(in: &disposeBag)
-//
-// let isComposeContentValid = composeViewModel.$characterCount
-// .map { characterCount -> Bool in
-// return characterCount <= ShareViewModel.composeContentLimit
-// }
-// let isMediaEmpty = composeViewModel.$attachmentViewModels
-// .map { $0.isEmpty }
-// let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels
-// .map { viewModels in
-// viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish }
-// }
-//
-// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
-// isComposeContentEmpty,
-// isComposeContentValid,
-// isMediaEmpty,
-// isMediaUploadAllSuccess
-// )
-// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
-// if isMediaEmpty {
-// return isComposeContentValid && !isComposeContentEmpty
-// } else {
-// return isComposeContentValid && isMediaUploadAllSuccess
-// }
-// }
-// .eraseToAnyPublisher()
-//
-// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest(
-// isComposeContentEmpty,
-// isComposeContentValid
-// )
-// .map { isComposeContentEmpty, isComposeContentValid -> Bool in
-// return isComposeContentValid && !isComposeContentEmpty
-// }
-// .eraseToAnyPublisher()
-//
-// Publishers.CombineLatest(
-// isPublishBarButtonItemEnabledPrecondition1,
-// isPublishBarButtonItemEnabledPrecondition2
-// )
-// .map { $0 && $1 }
-// .assign(to: \.value, on: isValid)
-// .store(in: &disposeBag)
-//
-// // bind counter
-// composeViewModel.$characterCount
-// .assign(to: \.value, on: characterCount)
-// .store(in: &disposeBag)
-//
-// // setup theme
-// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
-// ThemeService.shared.currentTheme
-// .receive(on: DispatchQueue.main)
-// .sink { [weak self] theme in
-// guard let self = self else { return }
-// self.setupBackgroundColor(theme: theme)
-// }
-// .store(in: &disposeBag)
- }
-
- private func setupBackgroundColor(theme: Theme) {
-// composeViewModel.contentWarningBackgroundColor = Color(theme.contentWarningOverlayBackgroundColor)
- }
-
-}
-
-//extension ShareViewModel {
-// enum ShareError: Error {
-// case `internal`(error: Error)
-// case userCancelShare
-// case missingAuthentication
-// }
-//}
-
-extension ComposeViewModel {
-// private func setupCoreData() {
-// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
-// DispatchQueue.global().async {
-// let _coreDataStack = CoreDataStack()
-// self.coreDataStack = _coreDataStack
-// self.managedObjectContext = _coreDataStack.persistentContainer.viewContext
-//
-// _coreDataStack.didFinishLoad
-// .receive(on: RunLoop.main)
-// .sink { [weak self] didFinishLoad in
-// guard let self = self else { return }
-// guard didFinishLoad else { return }
-// guard let managedObjectContext = self.managedObjectContext else { return }
-//
-//
-// self.api = APIService(backgroundManagedObjectContext: _coreDataStack.newTaskContext())
-//
-// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication…")
-// managedObjectContext.perform {
-// do {
-// let request = MastodonAuthentication.sortedFetchRequest
-// let authentications = try managedObjectContext.fetch(request)
-// let authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first
-// guard let activeAuthentication = authentication else {
-// self.authentication.value = .failure(ShareError.missingAuthentication)
-// return
-// }
-// self.authentication.value = .success(activeAuthentication)
-// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication success \(activeAuthentication.userID)")
-// } catch {
-// self.authentication.value = .failure(ShareError.internal(error: error))
-// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication fail \(error.localizedDescription)")
-// assertionFailure(error.localizedDescription)
-// }
-// }
-// }
-// .store(in: &self.disposeBag)
-// }
-// }
-}
-
-//extension ShareViewModel {
-// func parse(inputItems: [NSExtensionItem]) {
-// var itemProviders: [NSItemProvider] = []
-//
-// for item in inputItems {
-// itemProviders.append(contentsOf: item.attachments ?? [])
-// }
-//
-// let _textProvider = itemProviders.first { provider in
-// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: [])
-// }
-//
-// let _urlProvider = itemProviders.first { provider in
-// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: [])
-// }
-//
-// let _movieProvider = itemProviders.first { provider in
-// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: [])
-// }
-//
-// let imageProviders = itemProviders.filter { provider in
-// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: [])
-// }
-//
-// Task { @MainActor in
-// async let text = ShareViewModel.loadText(textProvider: _textProvider)
-// async let url = ShareViewModel.loadURL(textProvider: _urlProvider)
-//
-// let content = await [text, url]
-// .compactMap { $0 }
-// .joined(separator: " ")
-// self.composeViewModel.statusContent = content
-// }
-//
-// guard let api = self.api else { return }
-//
-// if let movieProvider = _movieProvider {
-// composeViewModel.setupAttachmentViewModels([
-// StatusAttachmentViewModel(api: api, itemProvider: movieProvider)
-// ])
-// } else if !imageProviders.isEmpty {
-// let viewModels = imageProviders.map { provider in
-// StatusAttachmentViewModel(api: api, itemProvider: provider)
-// }
-// composeViewModel.setupAttachmentViewModels(viewModels)
-// }
-//
-// }
-//
-// private static func loadText(textProvider: NSItemProvider?) async -> String? {
-// guard let textProvider = textProvider else { return nil }
-// do {
-// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier)
-// guard let text = item as? String else { return nil }
-// return text
-// } catch {
-// return nil
-// }
-// }
-//
-// private static func loadURL(textProvider: NSItemProvider?) async -> String? {
-// guard let textProvider = textProvider else { return nil }
-// do {
-// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier)
-// guard let url = item as? URL else { return nil }
-// return url.absoluteString
-// } catch {
-// return nil
-// }
-// }
-//
-//}
-//
-//extension ShareViewModel {
-// func publish() -> AnyPublisher, Error> {
-// guard let authentication = composeViewModel.authentication,
-// let api = self.api
-// else {
-// return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
-// }
-// let authenticationBox = MastodonAuthenticationBox(
-// authenticationRecord: .init(objectID: authentication.objectID),
-// domain: authentication.domain,
-// userID: authentication.userID,
-// appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
-// userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
-// )
-//
-// let domain = authentication.domain
-// let attachmentViewModels = composeViewModel.attachmentViewModels
-// let mediaIDs = attachmentViewModels.compactMap { viewModel in
-// viewModel.attachment.value?.id
-// }
-// let sensitive: Bool = composeViewModel.isContentWarningComposing
-// let spoilerText: String? = {
-// let text = composeViewModel.contentWarningContent
-// guard !text.isEmpty else { return nil }
-// return text
-// }()
-// let visibility = selectedStatusVisibility.value.visibility
-//
-// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = {
-// var subscriptions: [AnyPublisher, Error>] = []
-// for attachmentViewModel in attachmentViewModels {
-// guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue }
-// let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines)
-// guard !description.isEmpty else { continue }
-// let query = Mastodon.API.Media.UpdateMediaQuery(
-// file: nil,
-// thumbnail: nil,
-// description: description,
-// focus: nil
-// )
-// let subscription = api.updateMedia(
-// domain: domain,
-// attachmentID: attachmentID,
-// query: query,
-// mastodonAuthenticationBox: authenticationBox
-// )
-// subscriptions.append(subscription)
-// }
-// return subscriptions
-// }()
-//
-// let status = composeViewModel.statusContent
-//
-// return Publishers.MergeMany(updateMediaQuerySubscriptions)
-// .collect()
-// .asyncMap { attachments in
-// let query = Mastodon.API.Statuses.PublishStatusQuery(
-// status: status,
-// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs,
-// pollOptions: nil,
-// pollExpiresIn: nil,
-// inReplyToID: nil,
-// sensitive: sensitive,
-// spoilerText: spoilerText,
-// visibility: visibility
-// )
-// return try await api.publishStatus(
-// domain: domain,
-// idempotencyKey: nil, // FIXME:
-// query: query,
-// authenticationBox: authenticationBox
-// )
-// }
-// .eraseToAnyPublisher()
-// }
-//}
diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift
new file mode 100644
index 000000000..7757452cd
--- /dev/null
+++ b/ShareActionExtension/Scene/ShareViewController.swift
@@ -0,0 +1,330 @@
+//
+// ShareViewController.swift
+// ShareActionExtension
+//
+// Created by MainasuK on 2022/11/13.
+//
+
+import os.log
+import UIKit
+import Combine
+import CoreDataStack
+import MastodonCore
+import MastodonUI
+import MastodonAsset
+import MastodonLocalization
+import UniformTypeIdentifiers
+
+final class ShareViewController: UIViewController {
+
+ let logger = Logger(subsystem: "ShareViewController", category: "ViewController")
+
+ var disposeBag = Set()
+
+ let context = AppContext()
+ private(set) lazy var viewModel = ShareViewModel(context: context)
+
+ let publishButton: UIButton = {
+ let button = RoundedEdgesButton(type: .custom)
+ button.cornerRadius = 10
+ button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height
+ button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
+ button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
+ return button
+ }()
+ private func configurePublishButtonApperance() {
+ publishButton.adjustsImageWhenHighlighted = false
+ publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
+ publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
+ publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
+ publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
+ }
+
+ private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:)))
+ private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
+ let barButtonItem = UIBarButtonItem(customView: publishButton)
+ publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)
+ return barButtonItem
+ }()
+ let activityIndicatorBarButtonItem: UIBarButtonItem = {
+ let indicatorView = UIActivityIndicatorView(style: .medium)
+ let barButtonItem = UIBarButtonItem(customView: indicatorView)
+ indicatorView.startAnimating()
+ return barButtonItem
+ }()
+
+ private var composeContentViewModel: ComposeContentViewModel?
+ private var composeContentViewController: ComposeContentViewController?
+
+ let notSignInLabel: UILabel = {
+ let label = UILabel()
+ label.font = .preferredFont(forTextStyle: .subheadline)
+ label.textColor = .secondaryLabel
+ label.text = "No Available Account" // TODO: i18n
+ return label
+ }()
+
+}
+
+extension ShareViewController {
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setupTheme(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value)
+ ThemeService.shared.currentTheme
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] theme in
+ guard let self = self else { return }
+ self.setupTheme(theme: theme)
+ }
+ .store(in: &disposeBag)
+
+ view.backgroundColor = .systemBackground
+ title = L10n.Scene.Compose.Title.newPost
+
+ navigationItem.leftBarButtonItem = cancelBarButtonItem
+ navigationItem.rightBarButtonItem = publishBarButtonItem
+
+ do {
+ guard let authContext = try setupAuthContext() else {
+ setupHintLabel()
+ return
+ }
+ viewModel.authContext = authContext
+ let composeContentViewModel = ComposeContentViewModel(
+ context: context,
+ authContext: authContext,
+ kind: .post
+ )
+ let composeContentViewController = ComposeContentViewController()
+ composeContentViewController.viewModel = composeContentViewModel
+ addChild(composeContentViewController)
+ composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(composeContentViewController.view)
+ NSLayoutConstraint.activate([
+ composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
+ composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ ])
+ composeContentViewController.didMove(toParent: self)
+
+ self.composeContentViewModel = composeContentViewModel
+ self.composeContentViewController = composeContentViewController
+
+ Task { @MainActor in
+ let inputItems = self.extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
+ await load(inputItems: inputItems)
+ } // end Task
+ } catch {
+ logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): error: \(error.localizedDescription)")
+ }
+
+ viewModel.$isPublishing
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] isBusy in
+ guard let self = self else { return }
+ self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem
+ }
+ .store(in: &disposeBag)
+ }
+
+ override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+ super.traitCollectionDidChange(previousTraitCollection)
+
+ configurePublishButtonApperance()
+ }
+}
+
+extension ShareViewController {
+ @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
+ logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
+
+ extensionContext?.cancelRequest(withError: NSError(domain: "org.joinmastodon.app.ShareActionExtension", code: -1))
+ }
+
+ @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
+ logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
+
+
+ Task { @MainActor in
+ viewModel.isPublishing = true
+ do {
+ guard let statusPublisher = try composeContentViewModel?.statusPublisher(),
+ let authContext = viewModel.authContext
+ else {
+ throw AppError.badRequest
+ }
+
+ _ = try await statusPublisher.publish(api: context.apiService, authContext: authContext)
+
+ self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal)
+ try await Task.sleep(nanoseconds: 1 * .second)
+
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+
+ } catch {
+ let alertController = UIAlertController.standardAlert(of: error)
+ present(alertController, animated: true)
+ return
+ }
+ viewModel.isPublishing = false
+
+ }
+ }
+}
+
+extension ShareViewController {
+ private func setupAuthContext() throws -> AuthContext? {
+ let request = MastodonAuthentication.activeSortedFetchRequest // use active order
+ let _authentication = try context.managedObjectContext.fetch(request).first
+ let _authContext = _authentication.flatMap { AuthContext(authentication: $0) }
+ return _authContext
+ }
+
+ private func setupHintLabel() {
+ notSignInLabel.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(notSignInLabel)
+ NSLayoutConstraint.activate([
+ notSignInLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ notSignInLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+ ])
+ }
+
+ private func setupTheme(theme: Theme) {
+ view.backgroundColor = theme.systemElevatedBackgroundColor
+
+ let barAppearance = UINavigationBarAppearance()
+ barAppearance.configureWithDefaultBackground()
+ barAppearance.backgroundColor = theme.navigationBarBackgroundColor
+ navigationItem.standardAppearance = barAppearance
+ navigationItem.compactAppearance = barAppearance
+ navigationItem.scrollEdgeAppearance = barAppearance
+ }
+
+ private func showDismissConfirmAlertController() {
+ let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension
+ let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in
+ self.extensionContext?.cancelRequest(withError: ShareError.userCancelShare)
+ }
+ alertController.addAction(discardAction)
+ let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil)
+ alertController.addAction(okAction)
+ self.present(alertController, animated: true, completion: nil)
+ }
+}
+
+// MARK: - UIAdaptivePresentationControllerDelegate
+extension ShareViewController: UIAdaptivePresentationControllerDelegate {
+
+ func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
+ return composeContentViewModel?.shouldDismiss ?? true
+ }
+
+ func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ showDismissConfirmAlertController()
+ }
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
+ }
+
+}
+
+extension ShareViewController {
+
+ private func load(inputItems: [NSExtensionItem]) async {
+ guard let composeContentViewModel = self.composeContentViewModel,
+ let authContext = viewModel.authContext
+ else {
+ assertionFailure()
+ return
+ }
+ var itemProviders: [NSItemProvider] = []
+
+ for item in inputItems {
+ itemProviders.append(contentsOf: item.attachments ?? [])
+ }
+
+ let _textProvider = itemProviders.first { provider in
+ return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: [])
+ }
+
+ let _urlProvider = itemProviders.first { provider in
+ return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: [])
+ }
+
+ let _movieProvider = itemProviders.first { provider in
+ return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: [])
+ }
+
+ let imageProviders = itemProviders.filter { provider in
+ return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: [])
+ }
+
+ async let text = ShareViewController.loadText(textProvider: _textProvider)
+ async let url = ShareViewController.loadURL(textProvider: _urlProvider)
+
+ let content = await [text, url]
+ .compactMap { $0 }
+ .joined(separator: " ")
+ // passby the viewModel `content` value
+ if !content.isEmpty {
+ composeContentViewModel.content = content + " "
+ composeContentViewModel.contentMetaText?.textView.insertText(content + " ")
+ }
+
+ if let movieProvider = _movieProvider {
+ let attachmentViewModel = AttachmentViewModel(
+ api: context.apiService,
+ authContext: authContext,
+ input: .itemProvider(movieProvider),
+ delegate: composeContentViewModel
+ )
+ composeContentViewModel.attachmentViewModels.append(attachmentViewModel)
+ } else if !imageProviders.isEmpty {
+ let attachmentViewModels = imageProviders.map { provider in
+ AttachmentViewModel(
+ api: context.apiService,
+ authContext: authContext,
+ input: .itemProvider(provider),
+ delegate: composeContentViewModel
+ )
+ }
+ composeContentViewModel.attachmentViewModels.append(contentsOf: attachmentViewModels)
+ }
+ }
+
+ private static func loadText(textProvider: NSItemProvider?) async -> String? {
+ guard let textProvider = textProvider else { return nil }
+ do {
+ let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier)
+ guard let text = item as? String else { return nil }
+ return text
+ } catch {
+ return nil
+ }
+ }
+
+ private static func loadURL(textProvider: NSItemProvider?) async -> String? {
+ guard let textProvider = textProvider else { return nil }
+ do {
+ let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier)
+ guard let url = item as? URL else { return nil }
+ return url.absoluteString
+ } catch {
+ return nil
+ }
+ }
+
+}
+
+extension ShareViewController {
+ enum ShareError: Error {
+ case `internal`(error: Error)
+ case userCancelShare
+ case missingAuthentication
+ }
+}
diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift
new file mode 100644
index 000000000..ef8e200a6
--- /dev/null
+++ b/ShareActionExtension/Scene/ShareViewModel.swift
@@ -0,0 +1,43 @@
+//
+// ShareViewModel.swift
+// MastodonShareAction
+//
+// Created by MainasuK Cirno on 2021-7-16.
+//
+
+import os.log
+import Foundation
+import Combine
+import CoreData
+import CoreDataStack
+import MastodonSDK
+import SwiftUI
+import UniformTypeIdentifiers
+import MastodonAsset
+import MastodonLocalization
+import MastodonUI
+import MastodonCore
+
+final class ShareViewModel {
+
+ let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
+
+ var disposeBag = Set()
+
+ // input
+ let context: AppContext
+ @Published var authContext: AuthContext?
+
+ @Published var isPublishing = false
+
+ // output
+
+ init(
+ context: AppContext
+ ) {
+ self.context = context
+ // end init
+
+ }
+
+}
diff --git a/ShareActionExtension/Scene/View/ComposeToolbarView.swift b/ShareActionExtension/Scene/View/ComposeToolbarView.swift
deleted file mode 100644
index 557706fd8..000000000
--- a/ShareActionExtension/Scene/View/ComposeToolbarView.swift
+++ /dev/null
@@ -1,263 +0,0 @@
-//
-// ComposeToolbarView.swift
-// ShareActionExtension
-//
-// Created by MainasuK Cirno on 2021-7-19.
-//
-
-import os.log
-import UIKit
-import Combine
-import MastodonSDK
-import MastodonUI
-import MastodonAsset
-import MastodonLocalization
-import MastodonCore
-import MastodonUI
-
-protocol ComposeToolbarViewDelegate: AnyObject {
- func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton)
- func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType)
-}
-
-final class ComposeToolbarView: UIView {
-
- var disposeBag = Set()
-
- static let toolbarButtonSize: CGSize = CGSize(width: 44, height: 44)
- static let toolbarHeight: CGFloat = 44
-
- weak var delegate: ComposeToolbarViewDelegate?
-
- let contentWarningButton: UIButton = {
- let button = HighlightDimmableButton()
- ComposeToolbarView.configureToolbarButtonAppearance(button: button)
- button.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal)
- button.accessibilityLabel = L10n.Scene.Compose.Accessibility.enableContentWarning
- return button
- }()
-
- let visibilityButton: UIButton = {
- let button = HighlightDimmableButton()
- ComposeToolbarView.configureToolbarButtonAppearance(button: button)
- button.setImage(UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)), for: .normal)
- button.accessibilityLabel = L10n.Scene.Compose.Accessibility.postVisibilityMenu
- return button
- }()
-
- let characterCountLabel: UILabel = {
- let label = UILabel()
- label.font = .systemFont(ofSize: 15, weight: .regular)
- label.text = "500"
- label.textColor = Asset.Colors.Label.secondary.color
- label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
- return label
- }()
-
- let activeVisibilityType = CurrentValueSubject(.public)
-
- override init(frame: CGRect) {
- super.init(frame: frame)
- _init()
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- _init()
- }
-
-}
-
-extension ComposeToolbarView {
-
- private func _init() {
- setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
- ThemeService.shared.currentTheme
- .receive(on: DispatchQueue.main)
- .sink { [weak self] theme in
- guard let self = self else { return }
- self.setupBackgroundColor(theme: theme)
- }
- .store(in: &disposeBag)
-
- let stackView = UIStackView()
- stackView.axis = .horizontal
- stackView.spacing = 0
- stackView.distribution = .fillEqually
- stackView.translatesAutoresizingMaskIntoConstraints = false
- addSubview(stackView)
- NSLayoutConstraint.activate([
- stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
- layoutMarginsGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 8), // tweak button margin offset
- ])
-
- let buttons = [
- contentWarningButton,
- visibilityButton,
- ]
- buttons.forEach { button in
- button.translatesAutoresizingMaskIntoConstraints = false
- stackView.addArrangedSubview(button)
- NSLayoutConstraint.activate([
- button.widthAnchor.constraint(equalToConstant: 44),
- button.heightAnchor.constraint(equalToConstant: 44),
- ])
- }
-
- characterCountLabel.translatesAutoresizingMaskIntoConstraints = false
- addSubview(characterCountLabel)
- NSLayoutConstraint.activate([
- characterCountLabel.topAnchor.constraint(equalTo: topAnchor),
- characterCountLabel.leadingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: 8),
- characterCountLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
- characterCountLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
- ])
- characterCountLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
-
- contentWarningButton.addTarget(self, action: #selector(ComposeToolbarView.contentWarningButtonDidPressed(_:)), for: .touchUpInside)
- visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
- visibilityButton.showsMenuAsPrimaryAction = true
-
- updateToolbarButtonUserInterfaceStyle()
-
- // update menu when selected visibility type changed
- activeVisibilityType
- .receive(on: RunLoop.main)
- .sink { [weak self] type in
- guard let self = self else { return }
- self.visibilityButton.menu = self.createVisibilityContextMenu(interfaceStyle: self.traitCollection.userInterfaceStyle)
- }
- .store(in: &disposeBag)
- }
-
- override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- super.traitCollectionDidChange(previousTraitCollection)
-
- updateToolbarButtonUserInterfaceStyle()
- }
-
-}
-
-extension ComposeToolbarView {
- private func setupBackgroundColor(theme: Theme) {
- backgroundColor = theme.composeToolbarBackgroundColor
- }
-}
-
-extension ComposeToolbarView {
- enum MediaSelectionType: String {
- case camera
- case photoLibrary
- case browse
- }
-
- enum VisibilitySelectionType: String, CaseIterable {
- case `public`
- // TODO: remove unlisted option from codebase
- // case unlisted
- case `private`
- case direct
-
- var title: String {
- switch self {
- case .public: return L10n.Scene.Compose.Visibility.public
- // case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
- case .private: return L10n.Scene.Compose.Visibility.private
- case .direct: return L10n.Scene.Compose.Visibility.direct
- }
- }
-
- func image(interfaceStyle: UIUserInterfaceStyle) -> UIImage {
- switch self {
- case .public: return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .medium))!
- // case .unlisted: return UIImage(systemName: "eye.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular))!
- case .private:
- switch interfaceStyle {
- case .light: return UIImage(systemName: "person.3", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
- default: return UIImage(systemName: "person.3.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium))!
- }
- case .direct: return UIImage(systemName: "at", withConfiguration: UIImage.SymbolConfiguration(pointSize: 19, weight: .regular))!
- }
- }
-
- var visibility: Mastodon.Entity.Status.Visibility {
- switch self {
- case .public: return .public
- // case .unlisted: return .unlisted
- case .private: return .private
- case .direct: return .direct
- }
- }
- }
-}
-
-extension ComposeToolbarView {
-
- private static func configureToolbarButtonAppearance(button: UIButton) {
- button.tintColor = ThemeService.tintColor
- button.setBackgroundImage(.placeholder(size: ComposeToolbarView.toolbarButtonSize, color: .systemFill), for: .highlighted)
- button.layer.masksToBounds = true
- button.layer.cornerRadius = 5
- button.layer.cornerCurve = .continuous
- }
-
- private func updateToolbarButtonUserInterfaceStyle() {
- switch traitCollection.userInterfaceStyle {
- case .light:
- contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
-
- case .dark:
- contentWarningButton.setImage(UIImage(systemName: "exclamationmark.shield.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal)
-
- default:
- assertionFailure()
- }
-
- visibilityButton.menu = createVisibilityContextMenu(interfaceStyle: traitCollection.userInterfaceStyle)
- }
-
- private func createVisibilityContextMenu(interfaceStyle: UIUserInterfaceStyle) -> UIMenu {
- let children: [UIMenuElement] = VisibilitySelectionType.allCases.map { type in
- let state: UIMenuElement.State = activeVisibilityType.value == type ? .on : .off
- return UIAction(title: type.title, image: type.image(interfaceStyle: interfaceStyle), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [weak self] action in
- guard let self = self else { return }
- os_log(.info, "%{public}s[%{public}ld], %{public}s: visibilitySelectionType: %s", ((#file as NSString).lastPathComponent), #line, #function, type.rawValue)
- self.delegate?.composeToolbarView(self, visibilityButtonDidPressed: self.visibilityButton, visibilitySelectionType: type)
- }
- }
- return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children)
- }
-
-}
-
-extension ComposeToolbarView {
-
- @objc private func contentWarningButtonDidPressed(_ sender: UIButton) {
- os_log(.info, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- delegate?.composeToolbarView(self, contentWarningButtonDidPressed: sender)
- }
-
-}
-
-#if canImport(SwiftUI) && DEBUG
-import SwiftUI
-
-struct ComposeToolbarView_Previews: PreviewProvider {
-
- static var previews: some View {
- UIViewPreview(width: 375) {
- let toolbarView = ComposeToolbarView()
- toolbarView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- toolbarView.widthAnchor.constraint(equalToConstant: 375).priority(.defaultHigh),
- toolbarView.heightAnchor.constraint(equalToConstant: 64).priority(.defaultHigh),
- ])
- return toolbarView
- }
- .previewLayout(.fixed(width: 375, height: 100))
- }
-
-}
-
-#endif
-
diff --git a/ShareActionExtension/Scene/View/ComposeView.swift b/ShareActionExtension/Scene/View/ComposeView.swift
deleted file mode 100644
index a688d6492..000000000
--- a/ShareActionExtension/Scene/View/ComposeView.swift
+++ /dev/null
@@ -1,151 +0,0 @@
-//
-// ComposeView.swift
-//
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import UIKit
-import SwiftUI
-
-public struct ComposeView: View {
-
- @EnvironmentObject var viewModel: ComposeViewModel
- @State var statusEditorViewWidth: CGFloat = .zero
-
- let horizontalMargin: CGFloat = 20
-
- public init() { }
-
- public var body: some View {
- GeometryReader { proxy in
- List {
- // Content Warning
- if viewModel.isContentWarningComposing {
- ContentWarningEditorView(
- contentWarningContent: $viewModel.contentWarningContent,
- placeholder: viewModel.contentWarningPlaceholder
- )
- .padding(EdgeInsets(top: 6, leading: horizontalMargin, bottom: 6, trailing: horizontalMargin))
- .background(viewModel.contentWarningBackgroundColor)
- .transition(.opacity)
- .listRow(backgroundColor: Color(viewModel.backgroundColor))
- }
-
- // Author
- StatusAuthorView(
- avatarImageURL: viewModel.avatarImageURL,
- name: viewModel.authorName,
- username: viewModel.authorUsername
- )
- .padding(EdgeInsets(top: 20, leading: horizontalMargin, bottom: 16, trailing: horizontalMargin))
- .listRow(backgroundColor: Color(viewModel.backgroundColor))
-
- // Editor
- StatusEditorView(
- string: $viewModel.statusContent,
- placeholder: viewModel.statusPlaceholder,
- width: statusEditorViewWidth,
- attributedString: viewModel.statusContentAttributedString,
- keyboardType: .twitter,
- viewDidAppear: $viewModel.viewDidAppear
- )
- .frame(width: statusEditorViewWidth)
- .frame(minHeight: 100)
- .padding(EdgeInsets(top: 0, leading: horizontalMargin, bottom: 0, trailing: horizontalMargin))
- .listRow(backgroundColor: Color(viewModel.backgroundColor))
-
- // Attachments
- ForEach(viewModel.attachmentViewModels) { attachmentViewModel in
- let descriptionBinding = Binding {
- return attachmentViewModel.descriptionContent
- } set: { newValue in
- attachmentViewModel.descriptionContent = newValue
- }
-
- StatusAttachmentView(
- image: attachmentViewModel.thumbnailImage,
- descriptionPlaceholder: attachmentViewModel.descriptionPlaceholder,
- description: descriptionBinding,
- errorPrompt: attachmentViewModel.errorPrompt,
- errorPromptImage: attachmentViewModel.errorPromptImage,
- isUploading: attachmentViewModel.isUploading,
- progressViewTintColor: attachmentViewModel.progressViewTintColor,
- removeButtonAction: {
- self.viewModel.removeAttachmentViewModel(attachmentViewModel)
- }
- )
- }
- .padding(EdgeInsets(top: 16, leading: horizontalMargin, bottom: 0, trailing: horizontalMargin))
- .fixedSize(horizontal: false, vertical: true)
- .listRow(backgroundColor: Color(viewModel.backgroundColor))
-
- // bottom padding
- Color.clear
- .frame(height: viewModel.toolbarHeight + 20)
- .listRow(backgroundColor: Color(viewModel.backgroundColor))
- } // end List
- .listStyle(.plain)
- .introspectTableView(customize: { tableView in
- // tableView.keyboardDismissMode = .onDrag
- tableView.verticalScrollIndicatorInsets.bottom = viewModel.toolbarHeight
- })
- .preference(
- key: ComposeListViewFramePreferenceKey.self,
- value: proxy.frame(in: .local)
- )
- .onPreferenceChange(ComposeListViewFramePreferenceKey.self) { frame in
- var frame = frame
- frame.size.width = frame.width - 2 * horizontalMargin
- statusEditorViewWidth = frame.width
- } // end List
- .introspectTableView(customize: { tableView in
- tableView.backgroundColor = .clear
- })
- .overrideBackground(color: Color(viewModel.backgroundColor))
- } // end GeometryReader
- } // end body
-}
-
-struct ComposeListViewFramePreferenceKey: PreferenceKey {
- static var defaultValue: CGRect = .zero
- static func reduce(value: inout CGRect, nextValue: () -> CGRect) { }
-}
-
-extension View {
- // hack for separator line
- @ViewBuilder
- func listRow(backgroundColor: Color) -> some View {
- // expand list row to edge (set inset)
- // then hide the separator
- if #available(iOS 15, *) {
- frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
- .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1))
- .background(backgroundColor)
- .listRowSeparator(.hidden) // new API
- } else {
- frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
- .listRowInsets(EdgeInsets(top: -1, leading: -1, bottom: -1, trailing: -1)) // separator line hidden magic
- .background(backgroundColor)
- }
- }
-
- @ViewBuilder
- func overrideBackground(color: Color) -> some View {
- background(color.ignoresSafeArea())
- }
-}
-
-
-struct ComposeView_Previews: PreviewProvider {
-
- static let viewModel: ComposeViewModel = {
- let viewModel = ComposeViewModel()
- return viewModel
- }()
-
- static var previews: some View {
- ComposeView().environmentObject(viewModel)
- }
-
-}
diff --git a/ShareActionExtension/Scene/View/ComposeViewModel.swift b/ShareActionExtension/Scene/View/ComposeViewModel.swift
deleted file mode 100644
index 88c2b896f..000000000
--- a/ShareActionExtension/Scene/View/ComposeViewModel.swift
+++ /dev/null
@@ -1,130 +0,0 @@
-//
-// ComposeViewModel.swift
-// ShareActionExtension
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import Foundation
-import SwiftUI
-import Combine
-import CoreDataStack
-
-class ComposeViewModel: ObservableObject {
-
- var disposeBag = Set()
-
- @Published var authentication: MastodonAuthentication?
-
- @Published var backgroundColor: UIColor = .clear
- @Published var toolbarHeight: CGFloat = 0
- @Published var viewDidAppear = false
-
- @Published var avatarImageURL: URL?
- @Published var authorName: String = ""
- @Published var authorUsername: String = ""
-
- @Published var statusContent = ""
- @Published var statusPlaceholder = ""
- @Published var statusContentAttributedString = NSAttributedString()
-
- @Published var isContentWarningComposing = false
- @Published var contentWarningBackgroundColor = Color.secondary
- @Published var contentWarningPlaceholder = ""
- @Published var contentWarningContent = ""
-
- @Published private(set) var attachmentViewModels: [StatusAttachmentViewModel] = []
-
- @Published var characterCount = 0
-
- public init() {
- $statusContent
- .map { NSAttributedString(string: $0) }
- .assign(to: &$statusContentAttributedString)
-
- Publishers.CombineLatest3(
- $statusContent,
- $isContentWarningComposing,
- $contentWarningContent
- )
- .map { statusContent, isContentWarningComposing, contentWarningContent in
- var count = statusContent.count
- if isContentWarningComposing {
- count += contentWarningContent.count
- }
- return count
- }
- .assign(to: &$characterCount)
-
- // setup attribute updater
- $attachmentViewModels
- .receive(on: DispatchQueue.main)
- .debounce(for: 0.3, scheduler: DispatchQueue.main)
- .sink { attachmentViewModels in
- // drive upload state
- // make image upload in the queue
- for attachmentViewModel in attachmentViewModels {
- // skip when prefix N task when task finish OR fail OR uploading
- guard let currentState = attachmentViewModel.uploadStateMachine.currentState else { break }
- if currentState is StatusAttachmentViewModel.UploadState.Fail {
- continue
- }
- if currentState is StatusAttachmentViewModel.UploadState.Finish {
- continue
- }
- if currentState is StatusAttachmentViewModel.UploadState.Uploading {
- break
- }
- // trigger uploading one by one
- if currentState is StatusAttachmentViewModel.UploadState.Initial {
- attachmentViewModel.uploadStateMachine.enter(StatusAttachmentViewModel.UploadState.Uploading.self)
- break
- }
- }
- }
- .store(in: &disposeBag)
-
- #if DEBUG
- // avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
- // authorName = "Alice"
- // authorUsername = "alice"
- #endif
- }
-
-}
-
-extension ComposeViewModel {
- func setupAttachmentViewModels(_ viewModels: [StatusAttachmentViewModel]) {
- attachmentViewModels = viewModels
- for viewModel in viewModels {
- // set delegate
- viewModel.delegate = self
- // set observed
- viewModel.objectWillChange.sink { [weak self] _ in
- guard let self = self else { return }
- self.objectWillChange.send()
- }
- .store(in: &viewModel.disposeBag)
- // bind authentication
- $authentication
- .assign(to: \.value, on: viewModel.authentication)
- .store(in: &viewModel.disposeBag)
- }
- }
-
- func removeAttachmentViewModel(_ viewModel: StatusAttachmentViewModel) {
- if let index = attachmentViewModels.firstIndex(where: { $0 === viewModel }) {
- attachmentViewModels.remove(at: index)
- }
- }
-}
-
-// MARK: - StatusAttachmentViewModelDelegate
-extension ComposeViewModel: StatusAttachmentViewModelDelegate {
- func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?) {
- // trigger event update
- DispatchQueue.main.async {
- self.attachmentViewModels = self.attachmentViewModels
- }
- }
-}
diff --git a/ShareActionExtension/Scene/View/ContentWarningEditorView.swift b/ShareActionExtension/Scene/View/ContentWarningEditorView.swift
deleted file mode 100644
index 833c919fc..000000000
--- a/ShareActionExtension/Scene/View/ContentWarningEditorView.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// ContentWarningEditorView.swift
-//
-//
-// Created by MainasuK Cirno on 2021-7-19.
-//
-
-import SwiftUI
-import Introspect
-
-struct ContentWarningEditorView: View {
-
- @Binding var contentWarningContent: String
- let placeholder: String
- let spacing: CGFloat = 11
-
- var body: some View {
- HStack(alignment: .center, spacing: spacing) {
- Image(systemName: "exclamationmark.shield")
- .font(.system(size: 30, weight: .regular))
- Text(contentWarningContent.isEmpty ? " " : contentWarningContent)
- .opacity(0)
- .padding(.all, 8)
- .frame(maxWidth: .infinity)
- .overlay(
- TextEditor(text: $contentWarningContent)
- .introspectTextView { textView in
- textView.backgroundColor = .clear
- textView.placeholder = placeholder
- }
- )
- }
- }
-}
-
-struct ContentWarningEditorView_Previews: PreviewProvider {
-
- @State static var content = ""
-
- static var previews: some View {
- ContentWarningEditorView(
- contentWarningContent: $content,
- placeholder: "Write an accurate warning here..."
- )
- .previewLayout(.fixed(width: 375, height: 100))
- }
-}
-
diff --git a/ShareActionExtension/Scene/View/StatusAttachmentView.swift b/ShareActionExtension/Scene/View/StatusAttachmentView.swift
deleted file mode 100644
index 90b8aceeb..000000000
--- a/ShareActionExtension/Scene/View/StatusAttachmentView.swift
+++ /dev/null
@@ -1,126 +0,0 @@
-//
-// StatusAttachmentView.swift
-//
-//
-// Created by MainasuK Cirno on 2021-7-19.
-//
-
-import SwiftUI
-import Introspect
-
-struct StatusAttachmentView: View {
-
- let image: UIImage?
- let descriptionPlaceholder: String
- @Binding var description: String
- let errorPrompt: String?
- let errorPromptImage: UIImage
- let isUploading: Bool
- let progressViewTintColor: UIColor
-
- let removeButtonAction: () -> Void
-
- var body: some View {
- let image = image ?? UIImage.placeholder(color: .systemFill)
- ZStack(alignment: .bottom) {
- if let errorPrompt = errorPrompt {
- Color.clear
- .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
- .overlay(
- VStack(alignment: .center) {
- Image(uiImage: errorPromptImage)
- Text(errorPrompt)
- .lineLimit(2)
- }
- )
- .background(Color.gray)
- } else {
- Color.clear
- .aspectRatio(CGSize(width: 16, height: 9), contentMode: .fill)
- .overlay(
- Image(uiImage: image)
- .resizable()
- .aspectRatio(contentMode: .fill)
- )
- .background(Color.gray)
- LinearGradient(gradient: Gradient(colors: [Color(white: 0, opacity: 0.69), Color.clear]), startPoint: .bottom, endPoint: .top)
- .frame(maxHeight: 71)
- TextField("", text: $description)
- .placeholder(when: description.isEmpty) {
- Text(descriptionPlaceholder).foregroundColor(Color(white: 1, opacity: 0.6))
- .lineLimit(1)
- }
- .foregroundColor(.white)
- .font(.system(size: 15, weight: .regular, design: .default))
- .padding(EdgeInsets(top: 0, leading: 8, bottom: 7, trailing: 8))
- }
- }
- .cornerRadius(4)
- .badgeView(
- Button(action: {
- removeButtonAction()
- }, label: {
- Image(systemName: "minus.circle.fill")
- .renderingMode(.original)
- .font(.system(size: 22, weight: .bold, design: .default))
- })
- .buttonStyle(BorderlessButtonStyle())
- )
- .overlay(
- Group {
- if isUploading {
- ProgressView()
- .progressViewStyle(CircularProgressViewStyle(tint: Color(progressViewTintColor)))
- }
- }
- )
- }
-}
-
-extension View {
- func badgeView(_ content: Content) -> some View where Content: View {
- overlay(
- ZStack {
- content
- }
- .alignmentGuide(.top) { $0.height / 2 }
- .alignmentGuide(.trailing) { $0.width / 2 }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
- )
- }
-}
-
-/// ref: https://stackoverflow.com/a/57715771/3797903
-extension View {
- func placeholder(
- when shouldShow: Bool,
- alignment: Alignment = .leading,
- @ViewBuilder placeholder: () -> Content) -> some View {
-
- ZStack(alignment: alignment) {
- placeholder().opacity(shouldShow ? 1 : 0)
- self
- }
- }
-}
-
-
-//struct StatusAttachmentView_Previews: PreviewProvider {
-// static var previews: some View {
-// ScrollView {
-// StatusAttachmentView(
-// image: UIImage(systemName: "photo"),
-// descriptionPlaceholder: "Describe photo",
-// description: .constant(""),
-// errorPrompt: nil,
-// errorPromptImage: StatusAttachmentViewModel.photoFillSplitImage,
-// isUploading: true,
-// progressViewTintColor: .systemFill,
-// removeButtonAction: {
-// // do nothing
-// }
-// )
-// .padding(20)
-// }
-// }
-//}
diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift
deleted file mode 100644
index 56942cde0..000000000
--- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel+UploadState.swift
+++ /dev/null
@@ -1,131 +0,0 @@
-//
-// StatusAttachmentViewModel+UploadState.swift
-// ShareActionExtension
-//
-// Created by MainasuK Cirno on 2021-7-20.
-//
-
-import os.log
-import Foundation
-import Combine
-import GameplayKit
-import MastodonSDK
-import MastodonCore
-
-extension StatusAttachmentViewModel {
- class UploadState: GKState {
- weak var viewModel: StatusAttachmentViewModel?
-
- init(viewModel: StatusAttachmentViewModel) {
- 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)
- viewModel?.uploadStateMachineSubject.send(self)
- }
- }
-}
-
-extension StatusAttachmentViewModel.UploadState {
-
- class Initial: StatusAttachmentViewModel.UploadState {
- override func isValidNextState(_ stateClass: AnyClass) -> Bool {
- guard viewModel?.authentication.value != nil else { return false }
- if stateClass == Initial.self {
- return true
- }
-
- if viewModel?.file.value != nil {
- return stateClass == Uploading.self
- } else {
- return stateClass == Fail.self
- }
- }
- }
-
- class Uploading: StatusAttachmentViewModel.UploadState {
- let logger = Logger(subsystem: "StatusAttachmentViewModel.UploadState.Uploading", category: "logic")
- var needsFallback = false
-
- override func isValidNextState(_ stateClass: AnyClass) -> Bool {
- return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self
- }
-
- override func didEnter(from previousState: GKState?) {
- super.didEnter(from: previousState)
-
- guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
- guard let authentication = viewModel.authentication.value else { return }
- guard let file = viewModel.file.value else { return }
-
- let description = viewModel.descriptionContent
- let query = Mastodon.API.Media.UploadMediaQuery(
- file: file,
- thumbnail: nil,
- description: description,
- focus: nil
- )
-
- let mastodonAuthenticationBox = MastodonAuthenticationBox(
- authenticationRecord: .init(objectID: authentication.objectID),
- domain: authentication.domain,
- userID: authentication.userID,
- appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
- userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
- )
-
- // and needs clone the `query` if needs retry
- viewModel.api.uploadMedia(
- domain: mastodonAuthenticationBox.domain,
- query: query,
- mastodonAuthenticationBox: mastodonAuthenticationBox,
- needsFallback: needsFallback
- )
- .receive(on: DispatchQueue.main)
- .sink { [weak self] completion in
- guard let self = self else { return }
- switch completion {
- case .failure(let error):
- if let apiError = error as? Mastodon.API.Error,
- apiError.httpResponseStatus == .notFound,
- self.needsFallback == false
- {
- self.needsFallback = true
- stateMachine.enter(Uploading.self)
- self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fallback to V1")
- } else {
- self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fail: \(error.localizedDescription)")
- viewModel.error = error
- stateMachine.enter(Fail.self)
- }
- case .finished:
- self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success")
- break
- }
- } receiveValue: { [weak self] response in
- guard let self = self else { return }
- self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment \(response.value.id) success, \(response.value.url ?? "")")
- viewModel.attachment.value = response.value
- stateMachine.enter(Finish.self)
- }
- .store(in: &viewModel.disposeBag)
- }
-
- }
-
- class Fail: StatusAttachmentViewModel.UploadState {
- override func isValidNextState(_ stateClass: AnyClass) -> Bool {
- // allow discard publishing
- return stateClass == Uploading.self || stateClass == Finish.self
- }
- }
-
- class Finish: StatusAttachmentViewModel.UploadState {
- override func isValidNextState(_ stateClass: AnyClass) -> Bool {
- return false
- }
- }
-
-}
-
diff --git a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift b/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift
deleted file mode 100644
index 19251d0be..000000000
--- a/ShareActionExtension/Scene/View/StatusAttachmentViewModel.swift
+++ /dev/null
@@ -1,227 +0,0 @@
-//
-// StatusAttachmentViewModel.swift
-// ShareActionExtension
-//
-// Created by MainasuK Cirno on 2021-7-19.
-//
-
-import os.log
-import Foundation
-import SwiftUI
-import Combine
-import CoreDataStack
-import MastodonSDK
-import MastodonUI
-import AVFoundation
-import GameplayKit
-import MobileCoreServices
-import UniformTypeIdentifiers
-import MastodonAsset
-import MastodonCore
-import MastodonLocalization
-
-protocol StatusAttachmentViewModelDelegate: AnyObject {
- func statusAttachmentViewModel(_ viewModel: StatusAttachmentViewModel, uploadStateDidChange state: StatusAttachmentViewModel.UploadState?)
-}
-
-final class StatusAttachmentViewModel: ObservableObject, Identifiable {
-
- static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
- static let videoSplashImage: UIImage = {
- let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
- return image
- }()
-
- let logger = Logger(subsystem: "StatusAttachmentViewModel", category: "logic")
-
- weak var delegate: StatusAttachmentViewModelDelegate?
- var disposeBag = Set()
-
- let id = UUID()
- let itemProvider: NSItemProvider
-
- // input
- let api: APIService
- let file = CurrentValueSubject(nil)
- let authentication = CurrentValueSubject(nil)
- @Published var descriptionContent = ""
-
- // output
- let attachment = CurrentValueSubject(nil)
- @Published var thumbnailImage: UIImage?
- @Published var descriptionPlaceholder = ""
- @Published var isUploading = true
- @Published var progressViewTintColor = UIColor.systemFill
- @Published var error: Error?
- @Published var errorPrompt: String?
- @Published var errorPromptImage: UIImage = StatusAttachmentViewModel.photoFillSplitImage
-
- private(set) lazy var uploadStateMachine: GKStateMachine = {
- // exclude timeline middle fetcher state
- let stateMachine = GKStateMachine(states: [
- UploadState.Initial(viewModel: self),
- UploadState.Uploading(viewModel: self),
- UploadState.Fail(viewModel: self),
- UploadState.Finish(viewModel: self),
- ])
- stateMachine.enter(UploadState.Initial.self)
- return stateMachine
- }()
- lazy var uploadStateMachineSubject = CurrentValueSubject(nil)
-
- init(
- api: APIService,
- itemProvider: NSItemProvider
- ) {
- self.api = api
- self.itemProvider = itemProvider
-
- // bind attachment from item provider
- Just(itemProvider)
- .receive(on: DispatchQueue.main)
- .flatMap { result -> AnyPublisher in
- if itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) {
- return ItemProviderLoader.loadImageData(from: result).eraseToAnyPublisher()
- }
- if itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) {
- return ItemProviderLoader.loadVideoData(from: result).eraseToAnyPublisher()
- }
- return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher()
- }
- .sink { [weak self] completion in
- guard let self = self else { return }
- switch completion {
- case .failure(let error):
- self.error = error
- self.uploadStateMachine.enter(UploadState.Fail.self)
- case .finished:
- break
- }
- } receiveValue: { [weak self] file in
- guard let self = self else { return }
- self.file.value = file
- self.uploadStateMachine.enter(UploadState.Initial.self)
- }
- .store(in: &disposeBag)
-
- // bind progress view tint color
- $thumbnailImage
- .receive(on: DispatchQueue.main)
- .map { image -> UIColor in
- guard let image = image else { return .systemFill }
- switch image.domainLumaCoefficientsStyle {
- case .light:
- return UIColor.black.withAlphaComponent(0.8)
- default:
- return UIColor.white.withAlphaComponent(0.8)
- }
- }
- .assign(to: &$progressViewTintColor)
-
- // bind description placeholder and error prompt image
- file
- .receive(on: DispatchQueue.main)
- .sink { [weak self] file in
- guard let self = self else { return }
- guard let file = file else { return }
- switch file {
- case .jpeg, .png, .gif:
- self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionPhoto
- self.errorPromptImage = StatusAttachmentViewModel.photoFillSplitImage
- case .other:
- self.descriptionPlaceholder = L10n.Scene.Compose.Attachment.descriptionVideo
- self.errorPromptImage = StatusAttachmentViewModel.videoSplashImage
- }
- }
- .store(in: &disposeBag)
-
- // bind thumbnail image
- file
- .receive(on: DispatchQueue.main)
- .map { file -> UIImage? in
- guard let file = file else {
- return nil
- }
-
- switch file {
- case .jpeg(let data), .png(let data):
- return data.flatMap { UIImage(data: $0) }
- case .gif:
- // TODO:
- return nil
- case .other(let url, _, _):
- guard let url = url, FileManager.default.fileExists(atPath: url.path) else { return nil }
- let asset = AVURLAsset(url: url)
- let assetImageGenerator = AVAssetImageGenerator(asset: asset)
- assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
- do {
- let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
- let image = UIImage(cgImage: cgImage)
- return image
- } catch {
- self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)")
- return nil
- }
- }
- }
- .assign(to: &$thumbnailImage)
-
- // bind state and error
- Publishers.CombineLatest(
- uploadStateMachineSubject,
- $error
- )
- .sink { [weak self] state, error in
- guard let self = self else { return }
- // trigger delegate
- self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: state)
-
- // set error prompt
- if let error = error {
- self.isUploading = false
- self.errorPrompt = error.localizedDescription
- } else {
- guard let state = state else { return }
- switch state {
- case is UploadState.Finish:
- self.isUploading = false
- case is UploadState.Fail:
- self.isUploading = false
- // FIXME: not display
- self.errorPrompt = {
- guard let file = self.file.value else {
- return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
- }
- switch file {
- case .jpeg, .png, .gif:
- return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
- case .other:
- return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
- }
- }()
- default:
- break
- }
- }
- }
- .store(in: &disposeBag)
-
- // trigger delegate when authentication get new value
- authentication
- .receive(on: DispatchQueue.main)
- .sink { [weak self] authentication in
- guard let self = self else { return }
- guard authentication != nil else { return }
- self.delegate?.statusAttachmentViewModel(self, uploadStateDidChange: self.uploadStateMachineSubject.value)
- }
- .store(in: &disposeBag)
- }
-
-}
-
-extension StatusAttachmentViewModel {
- enum AttachmentError: Error {
- case invalidAttachmentType
- case attachmentTooLarge
- }
-}
diff --git a/ShareActionExtension/Scene/View/StatusAuthorView.swift b/ShareActionExtension/Scene/View/StatusAuthorView.swift
deleted file mode 100644
index 24453abe2..000000000
--- a/ShareActionExtension/Scene/View/StatusAuthorView.swift
+++ /dev/null
@@ -1,45 +0,0 @@
-//
-// StatusAuthorView.swift
-//
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import SwiftUI
-import MastodonUI
-import Nuke
-import FLAnimatedImage
-
-struct StatusAuthorView: View {
-
- let avatarImageURL: URL?
- let name: String
- let username: String
-
- var body: some View {
- HStack(spacing: 5) {
- AnimatedImage(imageURL: avatarImageURL)
- .frame(width: 42, height: 42)
- .background(Color(UIColor.systemFill))
- .cornerRadius(4)
- VStack(alignment: .leading) {
- Text(name)
- .font(.headline)
- Text(username)
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- Spacer()
- }
- }
-}
-
-struct StatusAuthorView_Previews: PreviewProvider {
- static var previews: some View {
- StatusAuthorView(
- avatarImageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"),
- name: "Alice",
- username: "alice"
- )
- }
-}
diff --git a/ShareActionExtension/Scene/View/StatusEditorView.swift b/ShareActionExtension/Scene/View/StatusEditorView.swift
deleted file mode 100644
index f670f6601..000000000
--- a/ShareActionExtension/Scene/View/StatusEditorView.swift
+++ /dev/null
@@ -1,105 +0,0 @@
-//
-// StatusEditorView.swift
-//
-//
-// Created by MainasuK Cirno on 2021-7-16.
-//
-
-import UIKit
-import SwiftUI
-import UITextView_Placeholder
-
-public struct StatusEditorView: UIViewRepresentable {
-
- @Binding var string: String
- let placeholder: String
- let width: CGFloat
- let attributedString: NSAttributedString
- let keyboardType: UIKeyboardType
- @Binding var viewDidAppear: Bool
-
- public init(
- string: Binding,
- placeholder: String,
- width: CGFloat,
- attributedString: NSAttributedString,
- keyboardType: UIKeyboardType,
- viewDidAppear: Binding
- ) {
- self._string = string
- self.placeholder = placeholder
- self.width = width
- self.attributedString = attributedString
- self.keyboardType = keyboardType
- self._viewDidAppear = viewDidAppear
- }
-
- public func makeUIView(context: Context) -> UITextView {
- let textView = UITextView(frame: .zero)
- textView.placeholder = placeholder
-
- textView.isScrollEnabled = false
- textView.font = .preferredFont(forTextStyle: .body)
- textView.textColor = .label
- textView.keyboardType = keyboardType
- textView.delegate = context.coordinator
- textView.backgroundColor = .clear
-
- textView.translatesAutoresizingMaskIntoConstraints = false
- let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100)
- widthLayoutConstraint.priority = .required - 1
- context.coordinator.widthLayoutConstraint = widthLayoutConstraint
-
- return textView
- }
-
- public func updateUIView(_ textView: UITextView, context: Context) {
- // preserve currently selected text range to prevent cursor jump
- let currentlySelectedRange = textView.selectedRange
-
- // update content
- // textView.attributedText = attributedString
- textView.text = string
-
- // update layout
- context.coordinator.updateLayout(width: width)
-
- // set becomeFirstResponder
- if viewDidAppear {
- viewDidAppear = false
- textView.becomeFirstResponder()
- }
-
- // restore selected text range
- textView.selectedRange = currentlySelectedRange
- }
-
- public func makeCoordinator() -> Coordinator {
- Coordinator(self)
- }
-
- public class Coordinator: NSObject, UITextViewDelegate {
- var parent: StatusEditorView
- var widthLayoutConstraint: NSLayoutConstraint?
-
- init(_ parent: StatusEditorView) {
- self.parent = parent
- }
-
- public func textViewDidChange(_ textView: UITextView) {
- // prevent break IME input
- if textView.markedTextRange == nil {
- parent.string = textView.text
- }
- }
-
- func updateLayout(width: CGFloat) {
- guard let widthLayoutConstraint = widthLayoutConstraint else { return }
- widthLayoutConstraint.constant = width
- widthLayoutConstraint.isActive = true
- }
- }
-
-}
-
-
diff --git a/swiftgen.yml b/swiftgen.yml
index e9c21260a..967abe370 100644
--- a/swiftgen.yml
+++ b/swiftgen.yml
@@ -1,7 +1,7 @@
strings:
inputs:
- - MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.strings
- - MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict
+ - MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings
+ - MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict
outputs:
- templateName: structured-swift5
output: MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift
diff --git a/update_localization.sh b/update_localization.sh
index 09cfc21d6..87477bdaa 100755
--- a/update_localization.sh
+++ b/update_localization.sh
@@ -7,15 +7,28 @@ PODS_ROOT='Pods'
echo ${SRCROOT}
-# task 1 generate strings file
+# Task 1
+# here we use the template source as input to
+# generate strings so we could use new strings
+# before sync to Crowdin
+
+# clean Base.lproj
+rm -rf ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj
+# copy tempate sources
+mkdir ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj
+cp ${SRCROOT}/Localization/app.json ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/app.json
+cp ${SRCROOT}/Localization/ios-infoPlist.json ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/ios-infoPlist.json
+cp ${SRCROOT}/Localization/Localizable.stringsdict ${SRCROOT}/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict
+
+# Task 2 generate strings file
cd ${SRCROOT}/Localization/StringsConvertor
sh ./scripts/build.sh
-# task 2 copy strings file
+# Task 3 copy strings file
cp -R ${SRCROOT}/Localization/StringsConvertor/output/module/ ${SRCROOT}/MastodonSDK/Sources/MastodonLocalization/Resources
cp -R ${SRCROOT}/Localization/StringsConvertor/Intents/output/ ${SRCROOT}/MastodonIntent
-# task 3 swiftgen
+# Task 4 swiftgen
cd ${SRCROOT}
echo "${PODS_ROOT}/SwiftGen/bin/swiftgen"
if [[ -f "${PODS_ROOT}/SwiftGen/bin/swiftgen" ]] then
@@ -24,6 +37,6 @@ else
echo "Run 'bundle exec pod install' or update your CocoaPods installation."
fi
-#task 4 clean temp file
+# Task 5 clean temp file
rm -rf ${SRCROOT}/Localization/StringsConvertor/output
rm -rf ${SRCROOT}/Localization/StringsConvertor/intents/output