Merge pull request #447 from mastodon/release-1.4.3

Release v1.4.3
This commit is contained in:
CMK 2022-06-06 12:02:45 +08:00 committed by GitHub
commit cdf6c44aaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 1873 additions and 1803 deletions

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
</dict> </dict>
</plist> </plist>

View File

@ -27,6 +27,7 @@
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) - [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
- [Tabman](https://github.com/uias/Tabman) - [Tabman](https://github.com/uias/Tabman)
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS) - [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer) - [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController) - [TOCropViewController](https://github.com/TimOliver/TOCropViewController)

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Trouvez un serveur ou rejoignez le vôtre...", "placeholder": "Trouvez un serveur ou rejoignez le vôtre...",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "Rechercher des serveurs ou entrer une URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Recherche des serveurs disponibles...", "finding_servers": "Recherche des serveurs disponibles...",
@ -621,7 +621,7 @@
"whats_wrong_with_this_post": "Quest-ce qui ne va pas avec ce message ?", "whats_wrong_with_this_post": "Quest-ce qui ne va pas avec ce message ?",
"whats_wrong_with_this_account": "Quest-ce qui ne va pas avec ce compte ?", "whats_wrong_with_this_account": "Quest-ce qui ne va pas avec ce compte ?",
"whats_wrong_with_this_username": "Quest-ce qui ne va pas avec %s ?", "whats_wrong_with_this_username": "Quest-ce qui ne va pas avec %s ?",
"select_the_best_match": "Select the best match", "select_the_best_match": "Sélectionnez ce qui correspond le mieux",
"i_dont_like_it": "Je naime pas", "i_dont_like_it": "Je naime pas",
"it_is_not_something_you_want_to_see": "Cest quelque chose que vous ne souhaitez pas voir", "it_is_not_something_you_want_to_see": "Cest quelque chose que vous ne souhaitez pas voir",
"its_spam": "Cest du spam", "its_spam": "Cest du spam",
@ -655,8 +655,8 @@
"mute_user": "Masquer %s", "mute_user": "Masquer %s",
"you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas quiels ont été mis en sourdine.", "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas quiels ont été mis en sourdine.",
"block_user": "Bloquer %s", "block_user": "Bloquer %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 theyve been blocked.", "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Ils ne seront plus en mesure de suivre ou de voir vos messages, mais iels peuvent voir siels ont été bloqué·e·s.",
"while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s" "while_we_review_this_you_can_take_action_against_user": "Pendant que nous étudions votre requête, vous pouvez prendre des mesures contre %s"
} }
}, },
"preview": { "preview": {

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Buscar comunidades", "placeholder": "Buscar comunidades",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "Busca un servidor ou escribe URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Buscando servidores dispoñibles...", "finding_servers": "Buscando servidores dispoñibles...",
@ -462,22 +462,22 @@
} }
}, },
"follower": { "follower": {
"title": "follower", "title": "seguidora",
"footer": "Non se mostran seguidoras desde outros servidores." "footer": "Non se mostran seguidoras desde outros servidores."
}, },
"following": { "following": {
"title": "following", "title": "seguindo",
"footer": "Non se mostran os seguimentos desde outros servidores." "footer": "Non se mostran os seguimentos desde outros servidores."
}, },
"familiarFollowers": { "familiarFollowers": {
"title": "Followers you familiar", "title": "Seguimentos próximos",
"followed_by_names": "Followed by %s" "followed_by_names": "Seguimentos de %s"
}, },
"favorited_by": { "favorited_by": {
"title": "Favorited By" "title": "Favorecido por"
}, },
"reblogged_by": { "reblogged_by": {
"title": "Reblogged By" "title": "Promovido por"
}, },
"search": { "search": {
"title": "Procurar", "title": "Procurar",

View File

@ -124,8 +124,8 @@
} }
}, },
"status": { "status": {
"user_reblogged": "%s hanno condiviso", "user_reblogged": "%s ha condiviso",
"user_replied_to": "Rispondi a %s", "user_replied_to": "Risposta a %s",
"show_post": "Mostra il post", "show_post": "Mostra il post",
"show_user_profile": "Mostra il profilo dell'utente", "show_user_profile": "Mostra il profilo dell'utente",
"content_warning": "Avviso sul contenuto", "content_warning": "Avviso sul contenuto",
@ -343,7 +343,7 @@
"title": "Inizio", "title": "Inizio",
"navigation_bar_state": { "navigation_bar_state": {
"offline": "Non in linea", "offline": "Non in linea",
"new_posts": "Vedi nuovi post", "new_posts": "Vedi i nuovi post",
"published": "Pubblicato!", "published": "Pubblicato!",
"Publishing": "Pubblicazione post...", "Publishing": "Pubblicazione post...",
"accessibility": { "accessibility": {
@ -533,7 +533,7 @@
"notification_description": { "notification_description": {
"followed_you": "ti ha seguito", "followed_you": "ti ha seguito",
"favorited_your_post": "ha apprezzato il tuo post", "favorited_your_post": "ha apprezzato il tuo post",
"reblogged_your_post": "ha ripostato il tuo post", "reblogged_your_post": "ha condiviso il tuo post",
"mentioned_you": "ti ha menzionato", "mentioned_you": "ti ha menzionato",
"request_to_follow_you": "richiesta di seguirti", "request_to_follow_you": "richiesta di seguirti",
"poll_has_ended": "sondaggio terminato" "poll_has_ended": "sondaggio terminato"

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "サーバーを探す", "placeholder": "サーバーを探す",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "サーバーを検索またはURLを入力"
}, },
"empty_state": { "empty_state": {
"finding_servers": "利用可能なサーバーの検索...", "finding_servers": "利用可能なサーバーの検索...",
@ -617,46 +617,46 @@
"text_placeholder": "追加コメントを入力", "text_placeholder": "追加コメントを入力",
"reported": "報告済み", "reported": "報告済み",
"step_one": { "step_one": {
"step_1_of_4": "Step 1 of 4", "step_1_of_4": "ステップ 1/4",
"whats_wrong_with_this_post": "What's wrong with this post?", "whats_wrong_with_this_post": "この投稿のどこが問題ですか?",
"whats_wrong_with_this_account": "What's wrong with this account?", "whats_wrong_with_this_account": "このアカウントのどこが問題ですか?",
"whats_wrong_with_this_username": "What's wrong with %s?", "whats_wrong_with_this_username": "%sさんのどこが問題ですか",
"select_the_best_match": "Select the best match", "select_the_best_match": "最も近いものを選んでください",
"i_dont_like_it": "I dont like it", "i_dont_like_it": "興味がありません",
"it_is_not_something_you_want_to_see": "It is not something you want to see", "it_is_not_something_you_want_to_see": "見たくない内容の場合",
"its_spam": "Its spam", "its_spam": "これはスパムです",
"malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies", "malicious_links_fake_engagement_or_repetetive_replies": "悪意あるリンクや虚偽の情報、執拗な返信など",
"it_violates_server_rules": "It violates server rules", "it_violates_server_rules": "サーバーのルールに違反しています",
"you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules", "you_are_aware_that_it_breaks_specific_rules": "ルールに違反しているのを見つけた場合",
"its_something_else": "Its something else", "its_something_else": "その他",
"the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories" "the_issue_does_not_fit_into_other_categories": "当てはまる選択肢がない場合"
}, },
"step_two": { "step_two": {
"step_2_of_4": "Step 2 of 4", "step_2_of_4": "ステップ 2/4",
"which_rules_are_being_violated": "Which rules are being violated?", "which_rules_are_being_violated": "どのルールに違反していますか?",
"select_all_that_apply": "Select all that apply", "select_all_that_apply": "Select all that apply",
"i_just_dont_like_it": "I just dont like it" "i_just_dont_like_it": "I just dont like it"
}, },
"step_three": { "step_three": {
"step_3_of_4": "Step 3 of 4", "step_3_of_4": "ステップ 3/4",
"are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?", "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" "select_all_that_apply": "Select all that apply"
}, },
"step_four": { "step_four": {
"step_4_of_4": "Step 4 of 4", "step_4_of_4": "ステップ 4/4",
"is_there_anything_else_we_should_know": "Is there anything else we should know?" "is_there_anything_else_we_should_know": "その他に私たちに伝えておくべき事はありますか?"
}, },
"step_final": { "step_final": {
"dont_want_to_see_this": "Dont want to see this?", "dont_want_to_see_this": "Dont 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 dont like on Mastodon, you can remove the person from your experience.", "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you dont like on Mastodon, you can remove the person from your experience.",
"unfollow": "Unfollow", "unfollow": "フォロー解除",
"unfollowed": "Unfollowed", "unfollowed": "フォロー解除しました",
"unfollow_user": "Unfollow %s", "unfollow_user": "%sをフォロー解除",
"mute_user": "Mute %s", "mute_user": "%sをミュート",
"you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You wont see their posts or reblogs in your home feed. They wont know theyve been muted.", "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You wont see their posts or reblogs in your home feed. They wont know theyve been muted.",
"block_user": "Block %s", "block_user": "%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 theyve been blocked.", "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 theyve been blocked.",
"while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s" "while_we_review_this_you_can_take_action_against_user": "私たちが確認している間でも、あなたは%sさんに対して対応することができます。"
} }
}, },
"preview": { "preview": {

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Nadi timɣiwnin", "placeholder": "Nadi timɣiwnin",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "Nadi timɣiwnin neɣ sekcem URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Tifin n yiqeddacen yellan...", "finding_servers": "Tifin n yiqeddacen yellan...",
@ -251,7 +251,7 @@
}, },
"register": { "register": {
"title": "Aha ad nebdu asbadu ɣef %s", "title": "Aha ad nebdu asbadu ɣef %s",
"lets_get_you_set_up_on_domain": "Lets get you set up on %s", "lets_get_you_set_up_on_domain": "Aha ad nebdu asbadu ɣef %s",
"input": { "input": {
"avatar": { "avatar": {
"delete": "Kkes" "delete": "Kkes"
@ -322,7 +322,7 @@
"confirm_email": { "confirm_email": {
"title": "Taɣawsa taneggarut.", "title": "Taɣawsa taneggarut.",
"subtitle": "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik.", "subtitle": "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik.",
"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": "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik",
"button": { "button": {
"open_email_app": "Ldi asnas n yimayl", "open_email_app": "Ldi asnas n yimayl",
"resend": "Ales tuzna" "resend": "Ales tuzna"
@ -347,7 +347,7 @@
"published": "Yettwasuffeɣ!", "published": "Yettwasuffeɣ!",
"Publishing": "Asuffeɣ tasuffeɣt...", "Publishing": "Asuffeɣ tasuffeɣt...",
"accessibility": { "accessibility": {
"logo_label": "Logo Button", "logo_label": "Taqeffalt n ulugu",
"logo_hint": "Tap to scroll to top and tap again to previous location" "logo_hint": "Tap to scroll to top and tap again to previous location"
} }
} }
@ -462,11 +462,11 @@
} }
}, },
"follower": { "follower": {
"title": "follower", "title": "aneḍfar",
"footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara." "footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."
}, },
"following": { "following": {
"title": "following", "title": "yeṭṭafar",
"footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara." "footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."
}, },
"familiarFollowers": { "familiarFollowers": {
@ -517,7 +517,7 @@
"posts": "Tisuffaɣ", "posts": "Tisuffaɣ",
"hashtags": "Ihacṭagen", "hashtags": "Ihacṭagen",
"news": "Isallen", "news": "Isallen",
"community": "Community", "community": "Tamɣiwent",
"for_you": "I kečč·kem" "for_you": "I kečč·kem"
}, },
"intro": "Tigi d tisuffaɣ i d-ijebbden s waṭas deg tama-inek•inem n Mastodon." "intro": "Tigi d tisuffaɣ i d-ijebbden s waṭas deg tama-inek•inem n Mastodon."
@ -617,46 +617,46 @@
"text_placeholder": "Aru neɣ senteḍ iwenniten-nniḍen", "text_placeholder": "Aru neɣ senteḍ iwenniten-nniḍen",
"reported": "YETTWAMMEL", "reported": "YETTWAMMEL",
"step_one": { "step_one": {
"step_1_of_4": "Step 1 of 4", "step_1_of_4": "Aḥric 1 seg 4",
"whats_wrong_with_this_post": "What's wrong with this post?", "whats_wrong_with_this_post": "Acu n wugur yellan d tsuffeɣt-a?",
"whats_wrong_with_this_account": "What's wrong with this account?", "whats_wrong_with_this_account": "What's wrong with this account?",
"whats_wrong_with_this_username": "What's wrong with %s?", "whats_wrong_with_this_username": "Acu n wugur yellan d %s?",
"select_the_best_match": "Select the best match", "select_the_best_match": "Fren amṣada akk igerrzen",
"i_dont_like_it": "I dont like it", "i_dont_like_it": "Ur ḥemmleɣ ara aya",
"it_is_not_something_you_want_to_see": "It is not something you want to see", "it_is_not_something_you_want_to_see": "D ayen akk ur bɣiɣ ara ad waliɣ",
"its_spam": "Its spam", "its_spam": "D aspam",
"malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies", "malicious_links_fake_engagement_or_repetetive_replies": "Yir iseɣwan, yir agman d tririyin i d-yettuɣalen",
"it_violates_server_rules": "It violates server rules", "it_violates_server_rules": "Truẓi n yilugan n uqeddac",
"you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules", "you_are_aware_that_it_breaks_specific_rules": "Teẓriḍ y•tettruẓu kra n yilugan",
"its_something_else": "Its something else", "its_something_else": "Ɣef ssebba-nniḍen",
"the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories" "the_issue_does_not_fit_into_other_categories": "Ugur ur yemṣada ara akk d taggayin-nniḍen"
}, },
"step_two": { "step_two": {
"step_2_of_4": "Step 2 of 4", "step_2_of_4": "Aḥric 2 seg 4",
"which_rules_are_being_violated": "Which rules are being violated?", "which_rules_are_being_violated": "Acu n yilugan i yettwarẓan?",
"select_all_that_apply": "Select all that apply", "select_all_that_apply": "Fren akk tifrat ara yettusnasen",
"i_just_dont_like_it": "I just dont like it" "i_just_dont_like_it": "Ur ḥemmleɣ ara kan aya"
}, },
"step_three": { "step_three": {
"step_3_of_4": "Step 3 of 4", "step_3_of_4": "Aḥric 3 seg 4",
"are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?", "are_there_any_posts_that_back_up_this_report": "Llant tsuffaɣ ara isdemren aneqqis-a?",
"select_all_that_apply": "Select all that apply" "select_all_that_apply": "Fren akk tifrat ara yettusnasen"
}, },
"step_four": { "step_four": {
"step_4_of_4": "Step 4 of 4", "step_4_of_4": "Aḥric 4 seg 4",
"is_there_anything_else_we_should_know": "Is there anything else we should know?" "is_there_anything_else_we_should_know": "Yella wayen-nniḍen i ilaqen ad t-nẓer?"
}, },
"step_final": { "step_final": {
"dont_want_to_see_this": "Dont want to see this?", "dont_want_to_see_this": "Ur tebɣiḍ ara ad twaliḍ aya?",
"when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you dont like on Mastodon, you can remove the person from your experience.", "when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "Mi ara twaliḍ kra ur ak•am-neɛǧib ara ɣef Mastodon, tzemreḍ ad tekkseḍ amdan-nni seg tirmit-ik•im.",
"unfollow": "Unfollow", "unfollow": "Ur ṭṭafaṛ ara",
"unfollowed": "Unfollowed", "unfollowed": "Y•Teḥbes aḍfar n",
"unfollow_user": "Unfollow %s", "unfollow_user": "Y•Teḥbes aḍfar n %s",
"mute_user": "Mute %s", "mute_user": "Sgugem %s",
"you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You wont see their posts or reblogs in your home feed. They wont know theyve been muted.", "you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "Ur tettwaliḍ ara tisuffaɣ-nsen neɣ iriblugen-nsen deg usuddem-inek•inem agejdan. Ur ẓerren ara belli tesgugmeḍ-ten.",
"block_user": "Block %s", "block_user": "Sewḥel %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 theyve been blocked.", "they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "Ur ttuɣalen ara ad izmiren ad ak•akem-ḍefren neɣ ad walin tisuffaɣ-inek•inem, maca ad walin ma yella ttusweḥlen.",
"while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s" "while_we_review_this_you_can_take_action_against_user": "Ideg nekkni nessenqad tuttra-inek•inem, tzemreḍ ad tḥadreḍ mgal %s"
} }
}, },
"preview": { "preview": {

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Li rajekaran bigere", "placeholder": "Li rajekaran bigere",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "Li rajekaran bigere an jî girêdanê têxe"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Peydakirina rajekarên berdest...", "finding_servers": "Peydakirina rajekarên berdest...",

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "ค้นหาเซิร์ฟเวอร์", "placeholder": "ค้นหาเซิร์ฟเวอร์",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "ค้นหาเซิร์ฟเวอร์หรือป้อน URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...", "finding_servers": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...",

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Tìm máy chủ", "placeholder": "Tìm máy chủ",
"search_servers_or_enter_url": "Search servers or enter URL" "search_servers_or_enter_url": "Tìm máy chủ hoặc nhập URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Đang tìm máy chủ hoạt động...", "finding_servers": "Đang tìm máy chủ hoạt động...",

View File

@ -241,7 +241,7 @@
}, },
"input": { "input": {
"placeholder": "Search servers", "placeholder": "Search servers",
"search_servers_or_enter_url": "Search communities or enter URL" "search_servers_or_enter_url": "Search servers or enter URL"
}, },
"empty_state": { "empty_state": {
"finding_servers": "Finding available servers...", "finding_servers": "Finding available servers...",

View File

@ -145,6 +145,8 @@
DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */; }; DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */; };
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */; }; DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */; };
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; }; DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */; };
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */; };
DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */; }; DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */; };
DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */; }; DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */; };
DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */; }; DB0FCB6E27950E6B006C02E2 /* MastodonMention.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */; };
@ -232,7 +234,6 @@
DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8F4281BB65200598866 /* MastodonSDK */; }; DB3EA8F5281BB65200598866 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8F4281BB65200598866 /* MastodonSDK */; };
DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FB281BBAE100598866 /* AlamofireImage */; }; DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FB281BBAE100598866 /* AlamofireImage */; };
DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FD281BBAF200598866 /* Alamofire */; }; DB3EA8FE281BBAF200598866 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FD281BBAF200598866 /* Alamofire */; };
DB3EA900281BBB1D00598866 /* MetaTextKit in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FF281BBB1D00598866 /* MetaTextKit */; };
DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA901281BBD5D00598866 /* CommonOSLog */; }; DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA901281BBD5D00598866 /* CommonOSLog */; };
DB3EA904281BBD9400598866 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA903281BBD9400598866 /* Introspect */; }; DB3EA904281BBD9400598866 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA903281BBD9400598866 /* Introspect */; };
DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA905281BBE8200598866 /* AlamofireImage */; }; DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA905281BBE8200598866 /* AlamofireImage */; };
@ -267,6 +268,7 @@
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; }; DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; };
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; };
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; }; DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; };
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
@ -376,6 +378,7 @@
DB697DDD278F521D004EF2F7 /* DataSourceFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */; }; DB697DDD278F521D004EF2F7 /* DataSourceFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */; };
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */; }; DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */; };
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */; }; DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */; };
DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */; };
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; }; DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; }; DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; }; DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; };
@ -507,7 +510,6 @@
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; }; DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; };
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; }; DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; };
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; };
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; }; DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */; };
@ -888,6 +890,8 @@
DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListContentView.swift; sourceTree = "<group>"; }; DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListContentView.swift; sourceTree = "<group>"; };
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; }; DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; }; DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; };
DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = "<group>"; };
DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = "<group>"; };
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = "<group>"; }; DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = "<group>"; };
DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMentionContainer.swift; sourceTree = "<group>"; }; DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMentionContainer.swift; sourceTree = "<group>"; };
DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMention.swift; sourceTree = "<group>"; }; DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMention.swift; sourceTree = "<group>"; };
@ -1136,6 +1140,7 @@
DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceFacade.swift; sourceTree = "<group>"; }; DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceFacade.swift; sourceTree = "<group>"; };
DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Profile.swift"; sourceTree = "<group>"; }; DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Profile.swift"; sourceTree = "<group>"; };
DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Model.swift"; sourceTree = "<group>"; }; DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Model.swift"; sourceTree = "<group>"; };
DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagerTabStripNavigateable.swift; sourceTree = "<group>"; };
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; }; DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = "<group>"; };
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; }; DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = "<group>"; };
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = "<group>"; }; DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = "<group>"; };
@ -1285,7 +1290,6 @@
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = "<group>"; }; DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = "<group>"; };
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = "<group>"; }; DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = "<group>"; };
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = "<group>"; }; DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = "<group>"; };
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; }; DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = "<group>"; }; DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = "<group>"; };
@ -1429,6 +1433,7 @@
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */,
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
@ -1469,7 +1474,6 @@
EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */,
DB3EA904281BBD9400598866 /* Introspect in Frameworks */, DB3EA904281BBD9400598866 /* Introspect in Frameworks */,
DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */, DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */,
DB3EA900281BBB1D00598866 /* MetaTextKit in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -1748,6 +1752,7 @@
DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */, DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */,
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */, DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */, DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */,
); );
path = Protocol; path = Protocol;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3004,8 +3009,8 @@
DB9D6C0825E4F5A60051B173 /* Profile */ = { DB9D6C0825E4F5A60051B173 /* Profile */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DBB525132611EBB1002F1F29 /* Segmented */,
DBB525462611ED57002F1F29 /* Header */, DBB525462611ED57002F1F29 /* Header */,
DBB525262611EBDA002F1F29 /* Paging */,
DBB5253B2611ECF5002F1F29 /* Timeline */, DBB5253B2611ECF5002F1F29 /* Timeline */,
DBE3CDF1261C6B3100430CC6 /* Favorite */, DBE3CDF1261C6B3100430CC6 /* Favorite */,
DB6B74F0272FB55400C70B6E /* Follower */, DB6B74F0272FB55400C70B6E /* Follower */,
@ -3104,15 +3109,6 @@
path = Video; path = Video;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
DBB525132611EBB1002F1F29 /* Segmented */ = {
isa = PBXGroup;
children = (
DBB525262611EBDA002F1F29 /* Paging */,
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */,
);
path = Segmented;
sourceTree = "<group>";
};
DBB525262611EBDA002F1F29 /* Paging */ = { DBB525262611EBDA002F1F29 /* Paging */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3148,6 +3144,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */, DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */,
DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */,
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */, DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */,
); );
path = View; path = View;
@ -3448,6 +3446,7 @@
DBA5A52E26F07ED800CACBAA /* PanModal */, DBA5A52E26F07ED800CACBAA /* PanModal */,
DB3EA911281BBEA800598866 /* AlamofireImage */, DB3EA911281BBEA800598866 /* AlamofireImage */,
DB3EA913281BBEA800598866 /* Alamofire */, DB3EA913281BBEA800598866 /* Alamofire */,
DB486C0E282E41F200F69423 /* TabBarPager */,
); );
productName = Mastodon; productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -3512,7 +3511,6 @@
DB3EA8F4281BB65200598866 /* MastodonSDK */, DB3EA8F4281BB65200598866 /* MastodonSDK */,
DB3EA8FB281BBAE100598866 /* AlamofireImage */, DB3EA8FB281BBAE100598866 /* AlamofireImage */,
DB3EA8FD281BBAF200598866 /* Alamofire */, DB3EA8FD281BBAF200598866 /* Alamofire */,
DB3EA8FF281BBB1D00598866 /* MetaTextKit */,
DB3EA901281BBD5D00598866 /* CommonOSLog */, DB3EA901281BBD5D00598866 /* CommonOSLog */,
DB3EA903281BBD9400598866 /* Introspect */, DB3EA903281BBD9400598866 /* Introspect */,
); );
@ -3665,11 +3663,11 @@
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */, DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */, DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */,
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */, DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */,
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */, DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */,
); );
productRefGroup = DB427DD325BAA00100D1B89D /* Products */; productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -4037,7 +4035,6 @@
DB0FCB7C2795821F006C02E2 /* StatusThreadRootTableViewCell.swift in Sources */, DB0FCB7C2795821F006C02E2 /* StatusThreadRootTableViewCell.swift in Sources */,
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */, DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
DB5B54A32833BD1A00DEF8B2 /* UserListViewModel.swift in Sources */, DB5B54A32833BD1A00DEF8B2 /* UserListViewModel.swift in Sources */,
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */, DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */,
DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */, DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */,
DB0FCB7E27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */, DB0FCB7E27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */,
@ -4256,6 +4253,7 @@
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */, DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */,
DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */, DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */,
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */,
DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
@ -4394,6 +4392,7 @@
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */, DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */, DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */,
DB9A489026035963008B817C /* APIService+Media.swift in Sources */, DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */,
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */, DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */,
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
@ -4402,6 +4401,7 @@
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */, DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */, DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
@ -4839,7 +4839,7 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -4869,7 +4869,7 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -4977,11 +4977,11 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 133; DYLIB_CURRENT_VERSION = 138;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -5008,11 +5008,11 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 133; DYLIB_CURRENT_VERSION = 138;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -5103,7 +5103,7 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -5171,11 +5171,11 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 133; DYLIB_CURRENT_VERSION = 138;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -5200,7 +5200,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5223,7 +5223,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist; INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5247,7 +5247,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist; INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5271,7 +5271,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist; INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5295,7 +5295,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist; INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5319,7 +5319,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist; INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5343,7 +5343,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist; INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5430,7 +5430,7 @@
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = Mastodon/Info.plist; INFOPLIST_FILE = Mastodon/Info.plist;
@ -5497,11 +5497,11 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 133; DYLIB_CURRENT_VERSION = 138;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = AppShared/Info.plist; INFOPLIST_FILE = AppShared/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@ -5525,7 +5525,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5548,7 +5548,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist; INFOPLIST_FILE = ShareActionExtension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5572,7 +5572,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = MastodonIntent/Info.plist; INFOPLIST_FILE = MastodonIntent/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5596,7 +5596,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5619,7 +5619,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 133; CURRENT_PROJECT_VERSION = 138;
DEVELOPMENT_TEAM = 5Z4GVSS33P; DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = NotificationService/Info.plist; INFOPLIST_FILE = NotificationService/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -5763,14 +5763,6 @@
minimumVersion = 0.1.1; minimumVersion = 0.1.1;
}; };
}; };
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/MetaTextKit.git";
requirement = {
kind = exactVersion;
version = 2.2.3;
};
};
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = { DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git"; repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git";
@ -5795,6 +5787,14 @@
minimumVersion = 5.4.0; minimumVersion = 5.4.0;
}; };
}; };
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/TabBarPager.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.0;
};
};
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = { DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git"; repositoryURL = "https://github.com/apple/swift-collections.git";
@ -5904,11 +5904,6 @@
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire; productName = Alamofire;
}; };
DB3EA8FF281BBB1D00598866 /* MetaTextKit */ = {
isa = XCSwiftPackageProductDependency;
package = DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */;
productName = MetaTextKit;
};
DB3EA901281BBD5D00598866 /* CommonOSLog */ = { DB3EA901281BBD5D00598866 /* CommonOSLog */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
@ -5959,6 +5954,11 @@
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire; productName = Alamofire;
}; };
DB486C0E282E41F200F69423 /* TabBarPager */ = {
isa = XCSwiftPackageProductDependency;
package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */;
productName = TabBarPager;
};
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = { DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */; package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;

View File

@ -9,7 +9,7 @@
<key>isShown</key> <key>isShown</key>
<true/> <true/>
<key>orderHint</key> <key>orderHint</key>
<integer>5</integer> <integer>9</integer>
</dict> </dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -24,22 +24,22 @@
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>8</integer> <integer>11</integer>
</dict> </dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key> <key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>4</integer>
</dict> </dict>
<key>Mastodon - Snapshot.xcscheme_^#shared#^_</key> <key>Mastodon - Snapshot.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>6</integer>
</dict> </dict>
<key>Mastodon - ar.xcscheme</key> <key>Mastodon - ar.xcscheme</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>8</integer>
</dict> </dict>
<key>Mastodon - ar.xcscheme_^#shared#^_</key> <key>Mastodon - ar.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -114,7 +114,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key> <key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>28</integer> <integer>30</integer>
</dict> </dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key> <key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -134,7 +134,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key> <key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>27</integer> <integer>31</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -105,8 +105,8 @@
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git", "repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "2b9556a78b2986b8c0b04adc6da8ec206b448a0c", "revision": "dcd5255d6930c2fab408dc8562c577547e477624",
"version": "2.2.3" "version": "2.2.5"
} }
}, },
{ {
@ -208,6 +208,15 @@
"version": "5.0.1" "version": "5.0.1"
} }
}, },
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"state": {
"branch": null,
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
"version": "0.1.0"
}
},
{ {
"package": "Tabman", "package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman", "repositoryURL": "https://github.com/uias/Tabman",

View File

@ -342,6 +342,7 @@ extension SceneCoordinator {
case .custom(let transitioningDelegate): case .custom(let transitioningDelegate):
viewController.modalPresentationStyle = .custom viewController.modalPresentationStyle = .custom
viewController.transitioningDelegate = transitioningDelegate viewController.transitioningDelegate = transitioningDelegate
// viewController.modalPresentationCapturesStatusBarAppearance = true
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil) (splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
case .customPush(let animated): case .customPush(let animated):

View File

@ -30,7 +30,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@ -43,7 +43,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View File

@ -0,0 +1,106 @@
//
// PagerTabStripNavigateable.swift
// Mastodon
//
// Created by MainasuK on 2022-6-2.
//
import UIKit
import XLPagerTabStrip
import MastodonLocalization
typealias PagerTabStripNavigateable = PagerTabStripNavigateableCore & PagerTabStripNavigateableRelay
protocol PagerTabStripNavigateableCore: AnyObject {
var navigateablePageViewController: PagerTabStripViewController { get }
var pagerTabStripNavigateKeyCommands: [UIKeyCommand] { get }
func pagerTabStripNavigateKeyCommandHandler(_ sender: UIKeyCommand)
func navigate(direction: PagerTabStripNavigationDirection)
}
@objc protocol PagerTabStripNavigateableRelay: AnyObject {
func pagerTabStripNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand)
}
enum PagerTabStripNavigationDirection: String, CaseIterable {
case previous
case next
var title: String {
switch self {
case .previous: return L10n.Common.Controls.Keyboard.SegmentedControl.previousSection
case .next: return L10n.Common.Controls.Keyboard.SegmentedControl.nextSection
}
}
// UIKeyCommand input
var input: String {
switch self {
case .previous: return "["
case .next: return "]"
}
}
var modifierFlags: UIKeyModifierFlags {
switch self {
case .previous: return [.shift, .command]
case .next: return [.shift, .command]
}
}
var propertyList: Any {
return rawValue
}
}
extension PagerTabStripNavigateableCore where Self: PagerTabStripNavigateableRelay {
var pagerTabStripNavigateKeyCommands: [UIKeyCommand] {
PagerTabStripNavigationDirection.allCases.map { direction in
UIKeyCommand(
title: direction.title,
image: nil,
action: #selector(Self.pagerTabStripNavigateKeyCommandHandlerRelay(_:)),
input: direction.input,
modifierFlags: direction.modifierFlags,
propertyList: direction.propertyList,
alternates: [],
discoverabilityTitle: nil,
attributes: [],
state: .off
)
}
}
func pagerTabStripNavigateKeyCommandHandler(_ sender: UIKeyCommand) {
guard let rawValue = sender.propertyList as? String,
let direction = PagerTabStripNavigationDirection(rawValue: rawValue) else { return }
navigate(direction: direction)
}
}
extension PagerTabStripNavigateableCore {
func navigate(direction: PagerTabStripNavigationDirection) {
let index = navigateablePageViewController.currentIndex
let targetIndex: Int
switch direction {
case .previous:
targetIndex = index - 1
case .next:
targetIndex = index + 1
}
guard targetIndex >= 0,
!navigateablePageViewController.viewControllers.isEmpty,
targetIndex < navigateablePageViewController.viewControllers.count,
navigateablePageViewController.canMoveTo(index: targetIndex)
else {
return
}
navigateablePageViewController.moveToViewController(at: targetIndex)
}
}

View File

@ -112,6 +112,12 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
@MainActor @MainActor
private func previewImage() async { private func previewImage() async {
guard let status = await statusRecord() else { return } guard let status = await statusRecord() else { return }
// workaround media preview not first responder issue
if let presentedViewController = presentedViewController as? MediaPreviewViewController {
presentedViewController.dismiss(animated: true, completion: nil)
return
}
guard let provider = self as? (DataSourceProvider & MediaPreviewableViewController) else { return } guard let provider = self as? (DataSourceProvider & MediaPreviewableViewController) else { return }
guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow, guard let indexPathForSelectedRow = tableView.indexPathForSelectedRow,

View File

@ -8,12 +8,12 @@
import UIKit import UIKit
protocol ScrollViewContainer: UIViewController { protocol ScrollViewContainer: UIViewController {
var scrollView: UIScrollView? { get } var scrollView: UIScrollView { get }
func scrollToTop(animated: Bool) func scrollToTop(animated: Bool)
} }
extension ScrollViewContainer { extension ScrollViewContainer {
func scrollToTop(animated: Bool) { func scrollToTop(animated: Bool) {
scrollView?.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated) scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
} }
} }

View File

@ -23,7 +23,6 @@ final class AccountListTableViewCell: UITableViewCell {
let checkmarkImageView: UIImageView = { let checkmarkImageView: UIImageView = {
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold)) let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold))
let imageView = UIImageView(image: image) let imageView = UIImageView(image: image)
imageView.tintColor = .label
return imageView return imageView
}() }()
let separatorLine = UIView.separatorLine let separatorLine = UIView.separatorLine

View File

@ -26,10 +26,14 @@ final class BadgeButton: UIButton {
extension BadgeButton { extension BadgeButton {
private func _init() { private func _init() {
titleLabel?.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) titleLabel?.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .medium))
setBackgroundColor(.systemBackground, for: .normal)
setTitleColor(.label, for: .normal)
contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6) contentEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6)
setAppearance()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setAppearance()
} }
override func layoutSubviews() { override func layoutSubviews() {
@ -39,6 +43,12 @@ extension BadgeButton {
layer.cornerRadius = frame.height * 0.5 layer.cornerRadius = frame.height * 0.5
} }
private func setAppearance() {
setBackgroundColor(Asset.Colors.Label.primary.color, for: .normal)
setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
tintColor = Asset.Colors.Label.primary.color
}
func setBadge(number: Int) { func setBadge(number: Int) {
let number = min(99, max(0, number)) let number = min(99, max(0, number))
setTitle("\(number)", for: .normal) setTitle("\(number)", for: .normal)

View File

@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryCommunityViewController: ScrollViewContainer { extension DiscoveryCommunityViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryCommunityViewController { extension DiscoveryCommunityViewController {

View File

@ -130,8 +130,8 @@ extension DiscoveryViewController {
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension DiscoveryViewController: ScrollViewContainer { extension DiscoveryViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView {
return (currentViewController as? ScrollViewContainer)?.scrollView return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
} }
} }

View File

@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryForYouViewController: ScrollViewContainer { extension DiscoveryForYouViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }

View File

@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryHashtagsViewController: ScrollViewContainer { extension DiscoveryHashtagsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryHashtagsViewController { extension DiscoveryHashtagsViewController {

View File

@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryNewsViewController: ScrollViewContainer { extension DiscoveryNewsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryNewsViewController { extension DiscoveryNewsViewController {

View File

@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryPostsViewController: ScrollViewContainer { extension DiscoveryPostsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
// MARK: - DiscoveryIntroBannerViewDelegate // MARK: - DiscoveryIntroBannerViewDelegate

View File

@ -537,13 +537,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension HomeTimelineViewController: ScrollViewContainer { extension HomeTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView } var scrollView: UIScrollView { return tableView }
func scrollToTop(animated: Bool) { func scrollToTop(animated: Bool) {
guard let scrollView = scrollView else {
return
}
if scrollView.contentOffset.y < scrollView.frame.height, if scrollView.contentOffset.y < scrollView.frame.height,
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,

View File

@ -135,6 +135,18 @@ extension MediaPreviewViewController {
} }
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// viewModel.$isPoping
// .receive(on: DispatchQueue.main)
// .removeDuplicates()
// .sink { [weak self] _ in
// guard let self = self else { return }
// // statusBar style update with animation
// self.setNeedsStatusBarAppearanceUpdate()
// UIView.animate(withDuration: 0.3) {
// }
// }
// .store(in: &disposeBag)
} }
} }

View File

@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension NotificationTimelineViewController: ScrollViewContainer { extension NotificationTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView { tableView }
var scrollView: UIScrollView? { tableView }
} }
extension NotificationTimelineViewController { extension NotificationTimelineViewController {

View File

@ -108,7 +108,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch statues failed: \(error.localizedDescription)") logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch statues failed: \(error.localizedDescription)")
await self.enter(state: Fail.self) await self.enter(state: Fail.self)
} }
} // Task } // end Task
} }
} }

View File

@ -88,7 +88,6 @@ extension NotificationTimelineViewModel {
} }
} }
var excludeTypes: [MastodonNotificationType]? { var excludeTypes: [MastodonNotificationType]? {
switch self { switch self {
case .everything: return nil case .everything: return nil

View File

@ -170,9 +170,9 @@ extension NotificationViewController {
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension NotificationViewController: ScrollViewContainer { extension NotificationViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView {
guard let viewController = currentViewController as? NotificationTimelineViewController else { guard let viewController = currentViewController as? NotificationTimelineViewController else {
return nil return UIScrollView()
} }
return viewController.scrollView return viewController.scrollView
} }

View File

@ -9,6 +9,9 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import MetaTextKit import MetaTextKit
import MastodonLocalization
import TabBarPager
import XLPagerTabStrip
protocol ProfileAboutViewControllerDelegate: AnyObject { protocol ProfileAboutViewControllerDelegate: AnyObject {
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
@ -162,7 +165,17 @@ extension ProfileAboutViewController: ProfileFieldEditCollectionViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension ProfileAboutViewController: ScrollViewContainer { extension ProfileAboutViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { collectionView }
collectionView }
// MARK: - TabBarPage
extension ProfileAboutViewController: TabBarPage {
var pageScrollView: UIScrollView { scrollView }
}
// MARK: - IndicatorInfoProvider
extension ProfileAboutViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: L10n.Scene.Profile.SegmentedControl.about)
} }
} }

View File

@ -25,7 +25,8 @@ extension ProfileAboutViewModel {
profileFieldEditCollectionViewCellDelegate: profileFieldEditCollectionViewCellDelegate profileFieldEditCollectionViewCellDelegate: profileFieldEditCollectionViewCellDelegate
) )
) )
self.diffableDataSource = diffableDataSource
diffableDataSource.reorderingHandlers.canReorderItem = { item -> Bool in diffableDataSource.reorderingHandlers.canReorderItem = { item -> Bool in
switch item { switch item {
case .editField: return true case .editField: return true
@ -42,22 +43,25 @@ extension ProfileAboutViewModel {
guard case let .editField(field) = item else { continue } guard case let .editField(field) = item else { continue }
fields.append(field) fields.append(field)
} }
self.editProfileInfo.fields = fields self.profileInfoEditing.fields = fields
} }
self.diffableDataSource = diffableDataSource
var snapshot = NSDiffableDataSourceSnapshot<ProfileFieldSection, ProfileFieldItem>()
snapshot.appendSections([.main])
diffableDataSource.apply(snapshot)
Publishers.CombineLatest4( Publishers.CombineLatest4(
$isEditing.removeDuplicates(), $isEditing.removeDuplicates(),
displayProfileInfo.$fields.removeDuplicates(), profileInfo.$fields.removeDuplicates(),
editProfileInfo.$fields.removeDuplicates(), profileInfoEditing.$fields.removeDuplicates(),
$emojiMeta.removeDuplicates() $emojiMeta.removeDuplicates()
) )
.throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true) .throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in .sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in
guard let self = self else { return } guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return } guard let diffableDataSource = self.diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<ProfileFieldSection, ProfileFieldItem>() var snapshot = NSDiffableDataSourceSnapshot<ProfileFieldSection, ProfileFieldItem>()
snapshot.appendSections([.main]) snapshot.appendSections([.main])
@ -69,17 +73,17 @@ extension ProfileAboutViewModel {
return ProfileFieldItem.field(field: field) return ProfileFieldItem.field(field: field)
} }
} }
if isEditing, fields.count < ProfileHeaderViewModel.maxProfileFieldCount { if isEditing, fields.count < ProfileHeaderViewModel.maxProfileFieldCount {
items.append(.addEntry) items.append(.addEntry)
} }
if !isEditing, items.isEmpty { if !isEditing, items.isEmpty {
items.append(.noResult) items.append(.noResult)
} }
snapshot.appendItems(items, toSection: .main) snapshot.appendItems(items, toSection: .main)
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
} }
.store(in: &disposeBag) .store(in: &disposeBag)

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
import Kanna import Kanna
@ -18,41 +19,69 @@ final class ProfileAboutViewModel {
// input // input
let context: AppContext let context: AppContext
@Published var user: MastodonUser?
@Published var isEditing = false @Published var isEditing = false
@Published var accountForEdit: Mastodon.Entity.Account? @Published var accountForEdit: Mastodon.Entity.Account?
@Published var emojiMeta: MastodonContent.Emojis = [:]
// output // output
var diffableDataSource: UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>? var diffableDataSource: UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>?
let profileInfo = ProfileInfo()
let profileInfoEditing = ProfileInfo()
let displayProfileInfo = ProfileInfo() @Published var fields: [MastodonField] = []
let editProfileInfo = ProfileInfo() @Published var emojiMeta: MastodonContent.Emojis = [:]
let editProfileInfoDidInitialized = CurrentValueSubject<Void, Never>(Void()) // needs trigger initial event
init(context: AppContext) { init(context: AppContext) {
self.context = context self.context = context
// end init // end init
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.emojis) }
.map { $0.asDictionary }
.assign(to: &$emojiMeta)
$user
.compactMap { $0 }
.flatMap { $0.publisher(for: \.fields) }
.assign(to: &$fields)
Publishers.CombineLatest( Publishers.CombineLatest(
$isEditing.removeDuplicates(), // only trigger when value toggle $fields,
$accountForEdit $emojiMeta
)
.map { fields, emojiMeta in
fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, emojiMeta: emojiMeta) }
}
.assign(to: &profileInfo.$fields)
Publishers.CombineLatest(
$accountForEdit,
$emojiMeta
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, account in .sink { [weak self] account, emojiMeta in
guard let self = self else { return } guard let self = self else { return }
guard isEditing else { return } guard let account = account else { return }
// setup editing value when toggle to editing self.profileInfo.fields = account.source?.fields?.compactMap { field in
self.editProfileInfo.fields = account?.source?.fields?.compactMap { field in ProfileFieldItem.FieldValue(
name: field.name,
value: field.value,
emojiMeta: emojiMeta
)
} ?? []
self.profileInfoEditing.fields = account.source?.fields?.compactMap { field in
ProfileFieldItem.FieldValue( ProfileFieldItem.FieldValue(
name: field.name, name: field.name,
value: field.value, value: field.value,
emojiMeta: [:] // no use for editing emojiMeta: [:] // no use for editing
) )
} ?? [] } ?? []
self.editProfileInfoDidInitialized.send()
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
} }
@ -65,31 +94,31 @@ extension ProfileAboutViewModel {
extension ProfileAboutViewModel { extension ProfileAboutViewModel {
func appendFieldItem() { func appendFieldItem() {
var fields = editProfileInfo.fields var fields = profileInfoEditing.fields
guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return } guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return }
fields.append(ProfileFieldItem.FieldValue(name: "", value: "", emojiMeta: [:])) fields.append(ProfileFieldItem.FieldValue(name: "", value: "", emojiMeta: [:]))
editProfileInfo.fields = fields profileInfoEditing.fields = fields
} }
func removeFieldItem(item: ProfileFieldItem) { func removeFieldItem(item: ProfileFieldItem) {
var fields = editProfileInfo.fields var fields = profileInfoEditing.fields
guard case let .editField(field) = item else { return } guard case let .editField(field) = item else { return }
guard let removeIndex = fields.firstIndex(of: field) else { return } guard let removeIndex = fields.firstIndex(of: field) else { return }
fields.remove(at: removeIndex) fields.remove(at: removeIndex)
editProfileInfo.fields = fields profileInfoEditing.fields = fields
} }
} }
// MARK: - ProfileViewModelEditable // MARK: - ProfileViewModelEditable
extension ProfileAboutViewModel: ProfileViewModelEditable { extension ProfileAboutViewModel: ProfileViewModelEditable {
func isEdited() -> Bool { var isEdited: Bool {
guard isEditing else { return false } guard isEditing else { return false }
let isFieldsEqual: Bool = { let isFieldsEqual: Bool = {
let originalFields = self.accountForEdit?.source?.fields?.compactMap { field in 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, emojiMeta: [:])
} ?? [] } ?? []
let editFields = editProfileInfo.fields let editFields = profileInfoEditing.fields
guard editFields.count == originalFields.count else { return false } guard editFields.count == originalFields.count else { return false }
for (editField, originalField) in zip(editFields, originalFields) { for (editField, originalField) in zip(editFields, originalFields) {
guard editField.name.value == originalField.name.value, guard editField.name.value == originalField.name.value,

View File

@ -8,6 +8,7 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import PhotosUI import PhotosUI
import AlamofireImage import AlamofireImage
import CropViewController import CropViewController
@ -15,22 +16,31 @@ import MastodonMeta
import MetaTextKit import MetaTextKit
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import Tabman import TabBarPager
protocol ProfileHeaderViewControllerDelegate: AnyObject { protocol ProfileHeaderViewControllerDelegate: AnyObject {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) func profileHeaderViewController(_ profileHeaderViewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
func profileHeaderViewController(_ profileHeaderViewController: ProfileHeaderViewController, profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta)
} }
final class ProfileHeaderViewController: UIViewController { final class ProfileHeaderViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
let logger = Logger(subsystem: "ProfileHeaderViewController", category: "ViewController")
static let segmentedControlHeight: CGFloat = 50 static let segmentedControlHeight: CGFloat = 50
static let headerMinHeight: CGFloat = segmentedControlHeight static let headerMinHeight: CGFloat = segmentedControlHeight
var disposeBag = Set<AnyCancellable>() weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var delegate: ProfileHeaderViewControllerDelegate? weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var viewModel: ProfileHeaderViewModel! var viewModel: ProfileHeaderViewModel!
weak var delegate: ProfileHeaderViewControllerDelegate?
weak var headerDelegate: TabBarPagerHeaderDelegate?
let mediaPreviewTransitionController = MediaPreviewTransitionController()
let titleView: DoubleTitleLabelNavigationBarTitleView = { let titleView: DoubleTitleLabelNavigationBarTitleView = {
let titleView = DoubleTitleLabelNavigationBarTitleView() let titleView = DoubleTitleLabelNavigationBarTitleView()
titleView.titleLabel.textColor = .white titleView.titleLabel.textColor = .white
@ -43,39 +53,8 @@ final class ProfileHeaderViewController: UIViewController {
}() }()
let profileHeaderView = ProfileHeaderView() let profileHeaderView = ProfileHeaderView()
let buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar()
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
buttonBar.backgroundView.style = .clear
buttonBar.layout.contentInset = .zero
return buttonBar
}()
func customizeButtonBarAppearance() { // private var isBannerPinned = false
// The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
// Needs trigger update when `userInterfaceStyle` chagnes
let userInterfaceStyle = traitCollection.userInterfaceStyle
buttonBar.buttons.customize { button in
switch userInterfaceStyle {
case .dark:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
default:
// Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
}
button.backgroundColor = .clear
}
}
private var isBannerPinned = false
private var bottomShadowAlpha: CGFloat = 0.0
// private var isAdjustBannerImageViewForSafeAreaInset = false // private var isAdjustBannerImageViewForSafeAreaInset = false
private var containerSafeAreaInset: UIEdgeInsets = .zero private var containerSafeAreaInset: UIEdgeInsets = .zero
@ -103,7 +82,7 @@ final class ProfileHeaderViewController: UIViewController {
}() }()
deinit { deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
} }
} }
@ -113,7 +92,7 @@ extension ProfileHeaderViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
customizeButtonBarAppearance() view.setContentHuggingPriority(.required - 1, for: .vertical)
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme ThemeService.shared.currentTheme
@ -124,137 +103,73 @@ extension ProfileHeaderViewController {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// profileHeaderView.preservesSuperviewLayoutMargins = true
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(profileHeaderView) view.addSubview(profileHeaderView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor), profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor), profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor), profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: profileHeaderView.bottomAnchor),
]) ])
profileHeaderView.preservesSuperviewLayoutMargins = true
Publishers.CombineLatest(
viewModel.viewDidAppear.eraseToAnyPublisher(),
viewModel.isTitleViewContentOffsetSet.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSet in
guard let self = self else { return }
self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0
self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSet ? 1 : 0
}
.store(in: &disposeBag)
viewModel.needsSetupBottomShadow
.receive(on: DispatchQueue.main)
.sink { [weak self] needsSetupBottomShadow in
guard let self = self else { return }
self.setupBottomShadow()
}
.store(in: &disposeBag)
Publishers.CombineLatest4(
viewModel.$isEditing.eraseToAnyPublisher(),
viewModel.displayProfileInfo.$avatarImageResource.eraseToAnyPublisher(),
viewModel.editProfileInfo.$avatarImageResource.eraseToAnyPublisher(),
viewModel.viewDidAppear.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, displayResource, editingResource, _ in
guard let self = self else { return }
let url = displayResource.url
let image = editingResource.image
self.profileHeaderView.avatarButton.avatarImageView.configure(
configuration: AvatarImageView.Configuration(
url: isEditing && image != nil ? nil : url,
placeholder: image ?? UIImage.placeholder(color: Asset.Theme.Mastodon.systemGroupedBackground.color)
)
)
}
.store(in: &disposeBag)
Publishers.CombineLatest4(
viewModel.$isEditing,
viewModel.displayProfileInfo.$name.removeDuplicates(),
viewModel.editProfileInfo.$name.removeDuplicates(),
viewModel.$emojiMeta
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, name, editingName, emojiMeta in
guard let self = self else { return }
do {
let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.profileHeaderView.nameMetaText.configure(content: metaContent)
} catch {
assertionFailure()
}
self.profileHeaderView.nameTextField.text = isEditing ? editingName : name
}
.store(in: &disposeBag)
let profileNote = Publishers.CombineLatest3(
viewModel.$isEditing.removeDuplicates(),
viewModel.displayProfileInfo.$note.removeDuplicates(),
viewModel.editProfileInfoDidInitialized
)
.map { isEditing, displayNote, _ -> String? in
if isEditing {
return self.viewModel.editProfileInfo.note
} else {
return displayNote
}
}
.eraseToAnyPublisher()
Publishers.CombineLatest3(
viewModel.$isEditing.removeDuplicates(),
profileNote.removeDuplicates(),
viewModel.$emojiMeta.removeDuplicates()
)
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, note, emojiMeta in
guard let self = self else { return }
self.profileHeaderView.bioMetaText.textView.isEditable = isEditing
if isEditing {
let metaContent = PlaintextMetaContent(string: note ?? "")
self.profileHeaderView.bioMetaText.configure(content: metaContent)
} else {
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
do {
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
self.profileHeaderView.bioMetaText.configure(content: metaContent)
} catch {
assertionFailure()
self.profileHeaderView.bioMetaText.reset()
}
}
}
.store(in: &disposeBag)
profileHeaderView.bioMetaText.delegate = self profileHeaderView.bioMetaText.delegate = self
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField) NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] notification in .sink { [weak self] notification in
guard let self = self else { return } guard let self = self else { return }
guard let textField = notification.object as? UITextField else { return } guard let textField = notification.object as? UITextField else { return }
self.viewModel.editProfileInfo.name = textField.text self.viewModel.profileInfoEditing.name = textField.text
} }
.store(in: &disposeBag) .store(in: &disposeBag)
profileHeaderView.editAvatarButton.menu = createAvatarContextMenu() profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createAvatarContextMenu()
profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true profileHeaderView.editAvatarButtonOverlayIndicatorView.showsMenuAsPrimaryAction = true
profileHeaderView.delegate = self
// bind viewModel
viewModel.$isTitleViewContentOffsetSet
.receive(on: DispatchQueue.main)
.sink { [weak self] isTitleViewContentOffsetDidSet in
guard let self = self else { return }
self.titleView.titleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0
self.titleView.subtitleLabel.alpha = isTitleViewContentOffsetDidSet ? 1 : 0
}
.store(in: &disposeBag)
viewModel.$user
.receive(on: DispatchQueue.main)
.sink { [weak self] user in
guard let self = self else { return }
guard let user = user else { return }
self.profileHeaderView.prepareForReuse()
self.profileHeaderView.configuration(user: user)
}
.store(in: &disposeBag)
viewModel.$relationshipActionOptionSet
.assign(to: \.relationshipActionOptionSet, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.$isEditing
.assign(to: \.isEditing, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.$isUpdating
.assign(to: \.isUpdating, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.profileInfoEditing.$avatar
.assign(to: \.avatarImageEditing, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.profileInfoEditing.$name
.assign(to: \.nameEditing, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
viewModel.profileInfoEditing.$note
.assign(to: \.noteEditing, on: profileHeaderView.viewModel)
.store(in: &disposeBag)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
viewModel.viewDidAppear.value = true profileHeaderView.viewModel.viewDidAppear.send()
// set display after view appear // set display after view appear
profileHeaderView.setupAvatarOverlayViews() profileHeaderView.setupAvatarOverlayViews()
} }
@ -262,14 +177,7 @@ extension ProfileHeaderViewController {
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view) headerDelegate?.viewLayoutDidUpdate(self)
setupBottomShadow()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
customizeButtonBarAppearance()
} }
} }
@ -321,56 +229,8 @@ extension ProfileHeaderViewController {
containerSafeAreaInset = inset containerSafeAreaInset = inset
} }
func setupBottomShadow() {
guard viewModel.needsSetupBottomShadow.value else {
view.layer.shadowColor = nil
view.layer.shadowRadius = 0
return
}
view.layer.setupShadow(color: UIColor.black.withAlphaComponent(0.12), alpha: Float(bottomShadowAlpha), x: 0, y: 2, blur: 2, spread: 0, roundedRect: view.bounds, byRoundingCorners: .allCorners, cornerRadii: .zero)
}
private func updateHeaderBottomShadow(progress: CGFloat) {
let alpha = min(max(0, 10 * progress - 9), 1)
if bottomShadowAlpha != alpha {
bottomShadowAlpha = alpha
view.setNeedsLayout()
}
}
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) { func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
updateHeaderBottomShadow(progress: progress)
let bannerImageView = profileHeaderView.bannerImageView
guard bannerImageView.bounds != .zero else {
// wait layout finish
return
}
let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
// scroll from bottom to top: 1 -> 2 -> 3
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
// 1
// banner top pin to window top and expand
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
// 3
// banner bottom pin to navigation bar bottom and
// the `progress` growth to 1 then segmented control pin to top
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
bannerImageView.frame.size.height = bannerImageHeight
} else {
// 2
// banner move with scrolling from bottom to top until the
// banner bottom higher than navigation bar bottom
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top
}
// set title view offset // set title view offset
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
@ -378,18 +238,14 @@ extension ProfileHeaderViewController {
let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
let transformY = max(0, titleViewContentOffset) let transformY = max(0, titleViewContentOffset)
titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY) titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height viewModel.isTitleViewDisplaying = transformY < titleView.containerView.frame.height
viewModel.isTitleViewContentOffsetSet = true
if viewModel.viewDidAppear.value { if progress > 0, throttle > 0 {
viewModel.isTitleViewContentOffsetSet.value = true // y = 1 - (x/t)
} // give: x = 0, y = 1
// x = t, y = 0
// set avatar fade let alpha = 1 - progress/throttle
if progress > 0 {
setProfileAvatar(alpha: 0)
} else if progress > -abs(throttle) {
// y = -(1/0.8T)x
let alpha = -1 / abs(0.8 * throttle) * progress
setProfileAvatar(alpha: alpha) setProfileAvatar(alpha: alpha)
} else { } else {
setProfileAvatar(alpha: 1) setProfileAvatar(alpha: 1)
@ -404,6 +260,103 @@ extension ProfileHeaderViewController {
} }
// MARK: - ProfileHeaderViewDelegate
extension ProfileHeaderViewController: ProfileHeaderViewDelegate {
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self,
user: record,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: button.avatarImageView,
containerView: .profileAvatar(profileHeaderView)
)
)
} // end Task
}
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, bannerImageViewDidPressed imageView: UIImageView) {
guard let user = viewModel.user else { return }
let record: ManagedObjectRecord<MastodonUser> = .init(objectID: user.objectID)
Task {
try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self,
user: record,
previewContext: DataSourceFacade.ImagePreviewContext(
imageView: imageView,
containerView: .profileBanner(profileHeaderView)
)
)
} // end Task
}
func profileHeaderView(
_ profileHeaderView: ProfileHeaderView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton
) {
delegate?.profileHeaderViewController(
self,
profileHeaderView: profileHeaderView,
relationshipButtonDidPressed: button
)
}
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) {
delegate?.profileHeaderViewController(
self,
profileHeaderView: profileHeaderView,
metaTextView: metaTextView,
metaDidPressed: meta
)
}
func profileHeaderView(
_ profileHeaderView: ProfileHeaderView,
profileStatusDashboardView dashboardView: ProfileStatusDashboardView,
dashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView,
meter: ProfileStatusDashboardView.Meter
) {
switch meter {
case .post:
// do nothing
break
case .follower:
guard let domain = viewModel.user?.domain,
let userID = viewModel.user?.id
else { return }
let followerListViewModel = FollowerListViewModel(
context: context,
domain: domain,
userID: userID
)
coordinator.present(
scene: .follower(viewModel: followerListViewModel),
from: self,
transition: .show
)
case .following:
guard let domain = viewModel.user?.domain,
let userID = viewModel.user?.id
else { return }
let followingListViewModel = FollowingListViewModel(
context: context,
domain: domain,
userID: userID
)
coordinator.present(
scene: .following(viewModel: followingListViewModel),
from: self,
transition: .show
)
}
}
}
// MARK: - MetaTextDelegate // MARK: - MetaTextDelegate
extension ProfileHeaderViewController: MetaTextDelegate { extension ProfileHeaderViewController: MetaTextDelegate {
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
@ -412,7 +365,9 @@ extension ProfileHeaderViewController: MetaTextDelegate {
switch metaText { switch metaText {
case profileHeaderView.bioMetaText: case profileHeaderView.bioMetaText:
guard viewModel.isEditing else { break } guard viewModel.isEditing else { break }
viewModel.editProfileInfo.note = metaText.backedString defer {
viewModel.profileInfoEditing.note = metaText.backedString
}
let metaContent = PlaintextMetaContent(string: metaText.backedString) let metaContent = PlaintextMetaContent(string: metaText.backedString)
return metaContent return metaContent
default: default:
@ -484,7 +439,10 @@ extension ProfileHeaderViewController: UIDocumentPickerDelegate {
// MARK: - CropViewControllerDelegate // MARK: - CropViewControllerDelegate
extension ProfileHeaderViewController: CropViewControllerDelegate { extension ProfileHeaderViewController: CropViewControllerDelegate {
public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) { public func cropViewController(_ cropViewController: CropViewController, didCropToImage image: UIImage, withRect cropRect: CGRect, angle: Int) {
viewModel.editProfileInfo.avatarImage = image viewModel.profileInfoEditing.avatar = image
cropViewController.dismiss(animated: true, completion: nil) cropViewController.dismiss(animated: true, completion: nil)
} }
} }
// MARK: - TabBarPagerHeader
extension ProfileHeaderViewController: TabBarPagerHeader { }

View File

@ -8,9 +8,11 @@
import os.log import os.log
import UIKit import UIKit
import Combine import Combine
import CoreDataStack
import Kanna import Kanna
import MastodonSDK import MastodonSDK
import MastodonMeta import MastodonMeta
import MastodonUI
final class ProfileHeaderViewModel { final class ProfileHeaderViewModel {
@ -21,39 +23,44 @@ final class ProfileHeaderViewModel {
// input // input
let context: AppContext let context: AppContext
@Published var user: MastodonUser?
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var isEditing = false @Published var isEditing = false
@Published var accountForEdit: Mastodon.Entity.Account? @Published var isUpdating = false
@Published var emojiMeta: MastodonContent.Emojis = [:]
let viewDidAppear = CurrentValueSubject<Bool, Never>(false) @Published var accountForEdit: Mastodon.Entity.Account?
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false) // let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
// output // output
let isTitleViewDisplaying = CurrentValueSubject<Bool, Never>(false) let profileInfo = ProfileInfo()
let displayProfileInfo = ProfileInfo() let profileInfoEditing = ProfileInfo()
let editProfileInfo = ProfileInfo()
let editProfileInfoDidInitialized = CurrentValueSubject<Void, Never>(Void()) // needs trigger initial event @Published var isTitleViewDisplaying = false
@Published var isTitleViewContentOffsetSet = false
init(context: AppContext) { init(context: AppContext) {
self.context = context self.context = context
Publishers.CombineLatest( $accountForEdit
$isEditing.removeDuplicates(), // only trigger when value toggle .receive(on: DispatchQueue.main)
$accountForEdit .sink { [weak self] account in
) guard let self = self else { return }
.receive(on: DispatchQueue.main) guard let account = account else { return }
.sink { [weak self] isEditing, account in // avatar
guard let self = self else { return } self.profileInfo.avatar = nil
guard isEditing else { return } self.profileInfoEditing.avatar = nil
// setup editing value when toggle to editing // name
self.editProfileInfo.name = self.displayProfileInfo.name // set to name let name = account.displayNameWithFallback
self.editProfileInfo.avatarImage = nil // set to empty self.profileInfo.name = name
self.editProfileInfo.note = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note) self.profileInfoEditing.name = name
self.editProfileInfoDidInitialized.send() // bio
} let note = ProfileHeaderViewModel.normalize(note: account.note)
.store(in: &disposeBag) self.profileInfo.note = note
self.profileInfoEditing.note = note
}
.store(in: &disposeBag)
} }
} }
@ -61,29 +68,9 @@ final class ProfileHeaderViewModel {
extension ProfileHeaderViewModel { extension ProfileHeaderViewModel {
class ProfileInfo { class ProfileInfo {
// input // input
@Published var avatar: UIImage?
@Published var name: String? @Published var name: String?
@Published var avatarImageURL: URL?
@Published var avatarImage: UIImage?
@Published var note: String? @Published var note: String?
// output
@Published var avatarImageResource = ImageResource(url: nil, image: nil)
struct ImageResource {
let url: URL?
let image: UIImage?
}
init() {
Publishers.CombineLatest(
$avatarImageURL,
$avatarImage
)
.map { url, image in
ImageResource(url: url, image: image)
}
.assign(to: &$avatarImageResource)
}
} }
} }
@ -103,15 +90,14 @@ extension ProfileHeaderViewModel {
} }
// MARK: - ProfileViewModelEditable // MARK: - ProfileViewModelEditable
extension ProfileHeaderViewModel: ProfileViewModelEditable { extension ProfileHeaderViewModel: ProfileViewModelEditable {
func isEdited() -> Bool { var isEdited: Bool {
guard isEditing else { return false } guard isEditing else { return false }
guard editProfileInfo.name == displayProfileInfo.name else { return true } guard profileInfoEditing.avatar == nil else { return true }
guard editProfileInfo.avatarImage == nil else { return true } guard profileInfo.name == profileInfoEditing.name else { return true }
guard editProfileInfo.note == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note) else { return true } guard profileInfo.note == profileInfoEditing.note else { return true }
return false return false
} }

View File

@ -0,0 +1,56 @@
//
// ProfileHeaderView+Configuration.swift
// Mastodon
//
// Created by MainasuK on 2022-5-26.
//
import os.log
import UIKit
import Combine
import CoreDataStack
extension ProfileHeaderView {
func configuration(user: MastodonUser) {
// header
user.publisher(for: \.header)
.map { _ in user.headerImageURL() }
.assign(to: \.headerImageURL, on: viewModel)
.store(in: &disposeBag)
// avatar
user.publisher(for: \.avatar)
.map { _ in user.avatarImageURL() }
.assign(to: \.avatarImageURL, on: viewModel)
.store(in: &disposeBag)
// emojiMeta
user.publisher(for: \.emojis)
.map { $0.asDictionary }
.assign(to: \.emojiMeta, on: viewModel)
.store(in: &disposeBag)
// name
user.publisher(for: \.displayName)
.map { _ in user.displayNameWithFallback }
.assign(to: \.name, on: viewModel)
.store(in: &disposeBag)
// username
viewModel.acct = user.acctWithDomain
// bio
user.publisher(for: \.note)
.assign(to: \.note, on: viewModel)
.store(in: &disposeBag)
// dashboard
user.publisher(for: \.statusesCount)
.map { Int($0) }
.assign(to: \.statusesCount, on: viewModel)
.store(in: &disposeBag)
user.publisher(for: \.followingCount)
.map { Int($0) }
.assign(to: \.followingCount, on: viewModel)
.store(in: &disposeBag)
user.publisher(for: \.followersCount)
.map { Int($0) }
.assign(to: \.followersCount, on: viewModel)
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,280 @@
//
// ProfileHeaderView+ViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-5-26.
//
import os.log
import UIKit
import Combine
import CoreDataStack
import MetaTextKit
import MastodonMeta
import MastodonUI
import MastodonAsset
import MastodonLocalization
extension ProfileHeaderView {
class ViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>()
let viewDidAppear = PassthroughSubject<Void, Never>()
@Published var state: State?
@Published var isEditing = false
@Published var isUpdating = false
@Published var emojiMeta: MastodonContent.Emojis = [:]
@Published var headerImageURL: URL?
@Published var avatarImageURL: URL?
@Published var avatarImageEditing: UIImage?
@Published var name: String?
@Published var nameEditing: String?
@Published var acct: String?
@Published var note: String?
@Published var noteEditing: String?
@Published var statusesCount: Int?
@Published var followingCount: Int?
@Published var followersCount: Int?
@Published var fields: [MastodonField] = []
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
@Published var isRelationshipActionButtonHidden = false
init() {
$relationshipActionOptionSet
.compactMap { $0.highPriorityAction(except: []) }
.map { $0 == .none }
.assign(to: &$isRelationshipActionButtonHidden)
}
}
}
extension ProfileHeaderView.ViewModel {
func bind(view: ProfileHeaderView) {
// header
Publishers.CombineLatest(
$headerImageURL,
viewDidAppear
)
.sink { headerImageURL, _ in
view.bannerImageView.af.cancelImageRequest()
let placeholder = UIImage.placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor)
guard let bannerImageURL = headerImageURL else {
view.bannerImageView.image = placeholder
return
}
view.bannerImageView.af.setImage(
withURL: bannerImageURL,
placeholderImage: placeholder,
imageTransition: .crossDissolve(0.3),
runImageTransitionIfCached: false,
completion: { [weak view] response in
guard let view = view else { return }
guard let image = response.value else { return }
guard image.size.width > 1 && image.size.height > 1 else {
// restore to placeholder when image invalid
view.bannerImageView.image = placeholder
return
}
}
)
}
.store(in: &disposeBag)
// avatar
Publishers.CombineLatest4(
$avatarImageURL,
$avatarImageEditing,
$isEditing,
viewDidAppear
)
.sink { avatarImageURL, avatarImageEditing, isEditing, _ in
view.avatarButton.avatarImageView.configure(configuration: .init(
url: (!isEditing || avatarImageEditing == nil) ? avatarImageURL : nil,
placeholder: isEditing ? (avatarImageEditing ?? AvatarImageView.placeholder) : AvatarImageView.placeholder
))
}
.store(in: &disposeBag)
// blur
$relationshipActionOptionSet
.map { $0.contains(.blocking) || $0.contains(.blockingBy) }
.sink { needsImageOverlayBlurred in
UIView.animate(withDuration: 0.33) {
let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
view.bannerImageViewOverlayVisualEffectView.effect = bannerEffect
let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil
view.avatarImageViewOverlayVisualEffectView.effect = avatarEffect
}
}
.store(in: &disposeBag)
// name
Publishers.CombineLatest4(
$isEditing.removeDuplicates(),
$name.removeDuplicates(),
$nameEditing.removeDuplicates(),
$emojiMeta.removeDuplicates()
)
.sink { isEditing, name, nameEditing, emojiMeta in
do {
let mastodonContent = MastodonContent(content: name ?? " ", emojis: emojiMeta)
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
view.nameMetaText.configure(content: metaContent)
} catch {
assertionFailure()
}
view.nameTextField.text = isEditing ? nameEditing : name
}
.store(in: &disposeBag)
// username
$acct
.map { acct in acct.flatMap { "@" + $0 } ?? " " }
.assign(to: \.text, on: view.usernameLabel)
.store(in: &disposeBag)
// bio
Publishers.CombineLatest4(
$isEditing.removeDuplicates(),
$emojiMeta.removeDuplicates(),
$note.removeDuplicates(),
$noteEditing.removeDuplicates()
)
.sink { isEditing, emojiMeta, note, noteEditing in
view.bioMetaText.textView.isEditable = isEditing
let metaContent: MetaContent = {
if isEditing {
return PlaintextMetaContent(string: noteEditing ?? "")
} else {
do {
let mastodonContent = MastodonContent(content: note ?? "", emojis: emojiMeta)
return try MastodonMetaContent.convert(document: mastodonContent)
} catch {
assertionFailure()
return PlaintextMetaContent(string: note ?? "")
}
}
}()
guard metaContent.string != view.bioMetaText.textStorage.string else { return }
view.bioMetaText.configure(content: metaContent)
}
.store(in: &disposeBag)
$relationshipActionOptionSet
.sink { optionSet in
let isBlocking = optionSet.contains(.blocking)
let isBlockedBy = optionSet.contains(.blockingBy)
let isSuspended = optionSet.contains(.suspended)
let isNeedsHidden = isBlocking || isBlockedBy || isSuspended
view.bioMetaText.textView.isHidden = isNeedsHidden
}
.store(in: &disposeBag)
// dashboard
$statusesCount
.sink { count in
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
view.statusDashboardView.postDashboardMeterView.numberLabel.text = text
view.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true
view.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0)
}
.store(in: &disposeBag)
$followingCount
.sink { count in
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
view.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
view.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true
view.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Plural.Count.following(count ?? 0)
}
.store(in: &disposeBag)
$followersCount
.sink { count in
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
view.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
view.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true
view.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Plural.Count.follower(count ?? 0)
}
.store(in: &disposeBag)
$isEditing
.sink { isEditing in
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
animator.addAnimations {
view.statusDashboardView.alpha = isEditing ? 0.2 : 1.0
}
animator.startAnimation()
}
.store(in: &disposeBag)
// relationship
$isRelationshipActionButtonHidden
.assign(to: \.isHidden, on: view.relationshipActionButtonShadowContainer)
.store(in: &disposeBag)
Publishers.CombineLatest3(
$relationshipActionOptionSet,
$isEditing,
$isUpdating
)
.sink { relationshipActionOptionSet, isEditing, isUpdating in
if relationshipActionOptionSet.contains(.edit) {
// check .edit state and set .editing when isEditing
view.relationshipActionButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
view.configure(state: isEditing ? .editing : .normal)
} else {
view.relationshipActionButton.configure(actionOptionSet: relationshipActionOptionSet)
}
}
.store(in: &disposeBag)
}
}
extension ProfileHeaderView {
enum State {
case normal
case editing
}
func configure(state: State) {
guard viewModel.state != state else { return } // avoid redundant animation
viewModel.state = state
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
switch state {
case .normal:
nameMetaText.textView.alpha = 1
nameTextField.alpha = 0
nameTextField.isEnabled = false
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
self.nameTextFieldBackgroundView.backgroundColor = .clear
self.editAvatarBackgroundView.alpha = 0
}
animator.addCompletion { _ in
self.editAvatarBackgroundView.isHidden = true
}
case .editing:
nameMetaText.textView.alpha = 0
nameTextField.isEnabled = true
nameTextField.alpha = 1
editAvatarBackgroundView.isHidden = false
editAvatarBackgroundView.alpha = 0
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
self.editAvatarBackgroundView.alpha = 1
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
}
}
animator.startAnimation()
}
}

View File

@ -38,8 +38,16 @@ final class ProfileHeaderView: UIView {
weak var delegate: ProfileHeaderViewDelegate? weak var delegate: ProfileHeaderViewDelegate?
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var state: State? func prepareForReuse() {
disposeBag.removeAll()
}
private(set) lazy var viewModel: ViewModel = {
let viewModel = ViewModel()
viewModel.bind(view: self)
return viewModel
}()
let bannerContainerView = UIView() let bannerContainerView = UIView()
let bannerImageView: UIImageView = { let bannerImageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
@ -61,6 +69,8 @@ final class ProfileHeaderView: UIView {
overlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor overlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
return overlayView return overlayView
}() }()
var bannerImageViewTopLayoutConstraint: NSLayoutConstraint!
var bannerImageViewBottomLayoutConstraint: NSLayoutConstraint!
let avatarImageViewBackgroundView: UIView = { let avatarImageViewBackgroundView: UIView = {
let view = UIView() let view = UIView()
@ -81,7 +91,7 @@ final class ProfileHeaderView: UIView {
func setupAvatarOverlayViews() { func setupAvatarOverlayViews() {
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6) editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
editAvatarButton.tintColor = .white editAvatarButtonOverlayIndicatorView.tintColor = .white
} }
static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark) static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark)
@ -101,7 +111,7 @@ final class ProfileHeaderView: UIView {
return view return view
}() }()
let editAvatarButton: HighlightDimmableButton = { let editAvatarButtonOverlayIndicatorView: HighlightDimmableButton = {
let button = HighlightDimmableButton() let button = HighlightDimmableButton()
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)), for: .normal) button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)), for: .normal)
button.tintColor = .clear button.tintColor = .clear
@ -136,7 +146,7 @@ final class ProfileHeaderView: UIView {
let nameTextField: UITextField = { let nameTextField: UITextField = {
let textField = UITextField() let textField = UITextField()
textField.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 22, weight: .bold)) textField.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 22, weight: .bold))
textField.textColor = Asset.Colors.Label.secondary.color textField.textColor = Asset.Colors.Label.primary.color
textField.text = "Alice" textField.text = "Alice"
textField.autocorrectionType = .no textField.autocorrectionType = .no
textField.autocapitalizationType = .none textField.autocapitalizationType = .none
@ -164,8 +174,8 @@ final class ProfileHeaderView: UIView {
return button return button
}() }()
let bioContainerView = UIView() // let bioContainerView = UIView()
let fieldContainerStackView = UIStackView() // let fieldContainerStackView = UIStackView()
let bioMetaText: MetaText = { let bioMetaText: MetaText = {
let metaText = MetaText() let metaText = MetaText()
@ -230,12 +240,19 @@ extension ProfileHeaderView {
bannerContainerView.topAnchor.constraint(equalTo: topAnchor), bannerContainerView.topAnchor.constraint(equalTo: topAnchor),
bannerContainerView.leadingAnchor.constraint(equalTo: leadingAnchor), bannerContainerView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingAnchor.constraint(equalTo: bannerContainerView.trailingAnchor), trailingAnchor.constraint(equalTo: bannerContainerView.trailingAnchor),
readableContentGuide.widthAnchor.constraint(equalTo: bannerContainerView.heightAnchor, multiplier: 3), // set height to 1/3 of readable frame width bannerContainerView.widthAnchor.constraint(equalTo: bannerContainerView.heightAnchor, multiplier: 3), // aspectRatio 1 : 3
]) ])
bannerImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] bannerImageView.translatesAutoresizingMaskIntoConstraints = false
bannerImageView.frame = bannerContainerView.bounds
bannerContainerView.addSubview(bannerImageView) bannerContainerView.addSubview(bannerImageView)
bannerImageViewTopLayoutConstraint = bannerImageView.topAnchor.constraint(equalTo: bannerContainerView.topAnchor)
bannerImageViewBottomLayoutConstraint = bannerContainerView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor)
NSLayoutConstraint.activate([
bannerImageViewTopLayoutConstraint,
bannerImageView.leadingAnchor.constraint(equalTo: bannerContainerView.leadingAnchor),
bannerImageView.trailingAnchor.constraint(equalTo: bannerContainerView.trailingAnchor),
bannerImageViewBottomLayoutConstraint,
])
bannerImageViewOverlayVisualEffectView.translatesAutoresizingMaskIntoConstraints = false bannerImageViewOverlayVisualEffectView.translatesAutoresizingMaskIntoConstraints = false
bannerImageView.addSubview(bannerImageViewOverlayVisualEffectView) bannerImageView.addSubview(bannerImageViewOverlayVisualEffectView)
@ -283,13 +300,13 @@ extension ProfileHeaderView {
editAvatarBackgroundView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor), editAvatarBackgroundView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor),
]) ])
editAvatarButton.translatesAutoresizingMaskIntoConstraints = false editAvatarButtonOverlayIndicatorView.translatesAutoresizingMaskIntoConstraints = false
editAvatarBackgroundView.addSubview(editAvatarButton) editAvatarBackgroundView.addSubview(editAvatarButtonOverlayIndicatorView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
editAvatarButton.topAnchor.constraint(equalTo: editAvatarBackgroundView.topAnchor), editAvatarButtonOverlayIndicatorView.topAnchor.constraint(equalTo: editAvatarBackgroundView.topAnchor),
editAvatarButton.leadingAnchor.constraint(equalTo: editAvatarBackgroundView.leadingAnchor), editAvatarButtonOverlayIndicatorView.leadingAnchor.constraint(equalTo: editAvatarBackgroundView.leadingAnchor),
editAvatarButton.trailingAnchor.constraint(equalTo: editAvatarBackgroundView.trailingAnchor), editAvatarButtonOverlayIndicatorView.trailingAnchor.constraint(equalTo: editAvatarBackgroundView.trailingAnchor),
editAvatarButton.bottomAnchor.constraint(equalTo: editAvatarBackgroundView.bottomAnchor), editAvatarButtonOverlayIndicatorView.bottomAnchor.constraint(equalTo: editAvatarBackgroundView.bottomAnchor),
]) ])
editAvatarBackgroundView.isUserInteractionEnabled = true editAvatarBackgroundView.isUserInteractionEnabled = true
avatarButton.isUserInteractionEnabled = true avatarButton.isUserInteractionEnabled = true
@ -297,6 +314,7 @@ extension ProfileHeaderView {
// container: V - [ dashboard container | author container | bio ] // container: V - [ dashboard container | author container | bio ]
let container = UIStackView() let container = UIStackView()
container.axis = .vertical container.axis = .vertical
container.distribution = .fill
container.spacing = 8 container.spacing = 8
container.preservesSuperviewLayoutMargins = true container.preservesSuperviewLayoutMargins = true
container.isLayoutMarginsRelativeArrangement = true container.isLayoutMarginsRelativeArrangement = true
@ -310,7 +328,7 @@ extension ProfileHeaderView {
layoutMarginsGuide.trailingAnchor.constraint(equalTo: container.trailingAnchor), layoutMarginsGuide.trailingAnchor.constraint(equalTo: container.trailingAnchor),
container.bottomAnchor.constraint(equalTo: bottomAnchor), container.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
// dashboardContainer: H - [ padding | statusDashboardView ] // dashboardContainer: H - [ padding | statusDashboardView ]
let dashboardContainer = UIStackView() let dashboardContainer = UIStackView()
dashboardContainer.axis = .horizontal dashboardContainer.axis = .horizontal
@ -364,6 +382,7 @@ extension ProfileHeaderView {
nameTextFieldBackgroundView.trailingAnchor.constraint(equalTo: nameMetaText.textView.trailingAnchor, constant: 5), nameTextFieldBackgroundView.trailingAnchor.constraint(equalTo: nameMetaText.textView.trailingAnchor, constant: 5),
nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextFieldBackgroundView.bottomAnchor), nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextFieldBackgroundView.bottomAnchor),
]) ])
// nameMetaText.textView.setContentHuggingPriority(, for: <#T##NSLayoutConstraint.Axis#>)
nameContainerStackView.addArrangedSubview(displayNameStackView) nameContainerStackView.addArrangedSubview(displayNameStackView)
nameContainerStackView.addArrangedSubview(usernameLabel) nameContainerStackView.addArrangedSubview(usernameLabel)
@ -438,53 +457,6 @@ extension ProfileHeaderView {
} }
extension ProfileHeaderView {
enum State {
case normal
case editing
}
func configure(state: State) {
guard self.state != state else { return } // avoid redundant animation
self.state = state
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
switch state {
case .normal:
nameMetaText.textView.alpha = 1
nameTextField.alpha = 0
nameTextField.isEnabled = false
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
self.nameTextFieldBackgroundView.backgroundColor = .clear
self.editAvatarBackgroundView.alpha = 0
}
animator.addCompletion { _ in
self.editAvatarBackgroundView.isHidden = true
}
case .editing:
nameMetaText.textView.alpha = 0
nameTextField.isEnabled = true
nameTextField.alpha = 1
editAvatarBackgroundView.isHidden = false
editAvatarBackgroundView.alpha = 0
bioMetaText.textView.backgroundColor = .clear
animator.addAnimations {
self.bannerImageViewOverlayVisualEffectView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundEditingColor
self.nameTextFieldBackgroundView.backgroundColor = Asset.Scene.Profile.Banner.nameEditBackgroundGray.color
self.editAvatarBackgroundView.alpha = 1
self.bioMetaText.textView.backgroundColor = Asset.Scene.Profile.Banner.bioEditBackgroundGray.color
}
}
animator.startAnimation()
}
}
extension ProfileHeaderView { extension ProfileHeaderView {
@objc private func relationshipActionButtonDidPressed(_ sender: UIButton) { @objc private func relationshipActionButtonDidPressed(_ sender: UIButton) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)

View File

@ -0,0 +1,217 @@
//
// ProfilePagingViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-29.
//
import os.log
import UIKit
import Combine
import XLPagerTabStrip
import TabBarPager
import MastodonAsset
protocol ProfilePagingViewControllerDelegate: AnyObject {
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
}
final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, TabBarPageViewController {
weak var tabBarPageViewDelegate: TabBarPageViewDelegate?
weak var pagingDelegate: ProfilePagingViewControllerDelegate?
var disposeBag = Set<AnyCancellable>()
var viewModel: ProfilePagingViewModel!
let buttonBarShadowView = UIView()
private var buttonBarShadowAlpha: CGFloat = 0.0
// MARK: - TabBarPageViewController
var currentPage: TabBarPage? {
return viewModel.viewControllers[currentIndex]
}
var currentPageIndex: Int? {
currentIndex
}
// MARK: - ButtonBarPagerTabStripViewController
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
return viewModel.viewControllers
}
override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged)
guard indexWasChanged else { return }
let page = viewModel.viewControllers[toIndex]
tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex)
}
// make key commands works
override var canBecomeFirstResponder: Bool {
return true
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ProfilePagingViewController {
override func viewDidLoad() {
// configure style before viewDidLoad
settings.style.buttonBarBackgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
settings.style.buttonBarItemBackgroundColor = .clear
settings.style.buttonBarItemsShouldFillAvailableWidth = false // alignment from leading to trailing
settings.style.selectedBarHeight = 3
settings.style.selectedBarBackgroundColor = Asset.Colors.Label.primary.color
settings.style.buttonBarItemFont = UIFont.systemFont(ofSize: 17, weight: .semibold)
changeCurrentIndexProgressive = { [weak self] (oldCell: ButtonBarViewCell?, newCell: ButtonBarViewCell?, progressPercentage: CGFloat, changeCurrentIndex: Bool, animated: Bool) -> Void in
guard let _ = self else { return }
guard changeCurrentIndex == true else { return }
oldCell?.label.textColor = Asset.Colors.Label.secondary.color
newCell?.label.textColor = Asset.Colors.Label.primary.color
}
super.viewDidLoad()
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.settings.style.buttonBarBackgroundColor = theme.systemBackgroundColor
self.barButtonLayout?.invalidateLayout()
}
.store(in: &disposeBag)
updateBarButtonInsets()
if let buttonBarView = self.buttonBarView {
buttonBarShadowView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(buttonBarShadowView, belowSubview: buttonBarView)
NSLayoutConstraint.activate([
buttonBarShadowView.topAnchor.constraint(equalTo: buttonBarView.topAnchor),
buttonBarShadowView.leadingAnchor.constraint(equalTo: buttonBarView.leadingAnchor),
buttonBarShadowView.trailingAnchor.constraint(equalTo: buttonBarView.trailingAnchor),
buttonBarShadowView.bottomAnchor.constraint(equalTo: buttonBarView.bottomAnchor),
])
viewModel.$needsSetupBottomShadow
.receive(on: DispatchQueue.main)
.sink { [weak self] needsSetupBottomShadow in
guard let self = self else { return }
self.setupBottomShadow()
}
.store(in: &disposeBag)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupBottomShadow()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateBarButtonInsets()
}
}
extension ProfilePagingViewController {
private func updateBarButtonInsets() {
let margin: CGFloat = {
switch traitCollection.userInterfaceIdiom {
case .phone:
return ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
default:
return traitCollection.horizontalSizeClass == .regular ?
ProfileViewController.containerViewMarginForRegularHorizontalSizeClass :
ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
}
}()
settings.style.buttonBarLeftContentInset = margin
settings.style.buttonBarRightContentInset = margin
barButtonLayout?.sectionInset.left = margin
barButtonLayout?.sectionInset.right = margin
barButtonLayout?.invalidateLayout()
}
private var barButtonLayout: UICollectionViewFlowLayout? {
let layout = buttonBarView.collectionViewLayout as? UICollectionViewFlowLayout
return layout
}
func setupBottomShadow() {
guard viewModel.needsSetupBottomShadow else {
buttonBarShadowView.layer.shadowColor = nil
buttonBarShadowView.layer.shadowRadius = 0
return
}
buttonBarShadowView.layer.setupShadow(
color: UIColor.black.withAlphaComponent(0.12),
alpha: Float(buttonBarShadowAlpha),
x: 0,
y: 2,
blur: 2,
spread: 0,
roundedRect: buttonBarShadowView.bounds,
byRoundingCorners: .allCorners,
cornerRadii: .zero
)
}
func updateButtonBarShadow(progress: CGFloat) {
let alpha = min(max(0, 10 * progress - 9), 1)
if buttonBarShadowAlpha != alpha {
buttonBarShadowAlpha = alpha
setupBottomShadow()
buttonBarShadowView.setNeedsLayout()
}
}
}
extension ProfilePagingViewController {
var currentViewController: (UIViewController & TabBarPage)? {
guard !viewModel.viewControllers.isEmpty,
currentIndex < viewModel.viewControllers.count
else { return nil }
return viewModel.viewControllers[currentIndex]
}
}
// workaround to fix tab man responder chain issue
extension ProfilePagingViewController {
override var keyCommands: [UIKeyCommand]? {
return currentViewController?.keyCommands
}
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
}
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
}
}

View File

@ -0,0 +1,50 @@
//
// ProfilePagingViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-29.
//
import os.log
import UIKit
import MastodonAsset
import MastodonLocalization
import TabBarPager
final class ProfilePagingViewModel: NSObject {
let postUserTimelineViewController = UserTimelineViewController()
let repliesUserTimelineViewController = UserTimelineViewController()
let mediaUserTimelineViewController = UserTimelineViewController()
let profileAboutViewController = ProfileAboutViewController()
// input
@Published var needsSetupBottomShadow = true
init(
postsUserTimelineViewModel: UserTimelineViewModel,
repliesUserTimelineViewModel: UserTimelineViewModel,
mediaUserTimelineViewModel: UserTimelineViewModel,
profileAboutViewModel: ProfileAboutViewModel
) {
postUserTimelineViewController.viewModel = postsUserTimelineViewModel
repliesUserTimelineViewController.viewModel = repliesUserTimelineViewModel
mediaUserTimelineViewController.viewModel = mediaUserTimelineViewModel
profileAboutViewController.viewModel = profileAboutViewModel
super.init()
}
var viewControllers: [UIViewController & TabBarPage] {
return [
postUserTimelineViewController,
repliesUserTimelineViewController,
mediaUserTimelineViewController,
profileAboutViewController,
]
}
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -27,97 +27,109 @@ class ProfileViewModel: NSObject {
private var mastodonUserObserver: AnyCancellable? private var mastodonUserObserver: AnyCancellable?
private var currentMastodonUserObserver: AnyCancellable? private var currentMastodonUserObserver: AnyCancellable?
let postsUserTimelineViewModel: UserTimelineViewModel
let repliesUserTimelineViewModel: UserTimelineViewModel
let mediaUserTimelineViewModel: UserTimelineViewModel
let profileAboutViewModel: ProfileAboutViewModel
// input // input
let context: AppContext let context: AppContext
@Published var me: MastodonUser? @Published var me: MastodonUser?
@Published var user: MastodonUser? @Published var user: MastodonUser?
let viewDidAppear = PassthroughSubject<Void, Never>() let viewDidAppear = PassthroughSubject<Void, Never>()
@Published var isEditing = false
@Published var isUpdating = false
@Published var accountForEdit: Mastodon.Entity.Account?
// output // output
let domain: CurrentValueSubject<String?, Never> let relationshipViewModel = RelationshipViewModel()
let userID: CurrentValueSubject<UserID?, Never>
let bannerImageURL: CurrentValueSubject<URL?, Never>
let avatarImageURL: CurrentValueSubject<URL?, Never>
let name: CurrentValueSubject<String?, Never>
let username: CurrentValueSubject<String?, Never>
let bioDescription: CurrentValueSubject<String?, Never>
let url: CurrentValueSubject<String?, Never>
let statusesCount: CurrentValueSubject<Int?, Never>
let followingCount: CurrentValueSubject<Int?, Never>
let followersCount: CurrentValueSubject<Int?, Never>
let fields: CurrentValueSubject<[MastodonField], Never>
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
// fulfill this before editing
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
let protected: CurrentValueSubject<Bool?, Never>
let suspended: CurrentValueSubject<Bool, Never>
let isEditing = CurrentValueSubject<Bool, Never>(false)
let isUpdating = CurrentValueSubject<Bool, Never>(false)
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none) @Published var userIdentifier: UserIdentifier? = nil
let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
let isMuting = CurrentValueSubject<Bool, Never>(false)
let isBlocking = CurrentValueSubject<Bool, Never>(false)
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true) @Published var isRelationshipActionButtonHidden: Bool = true
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true) @Published var isReplyBarButtonItemHidden: Bool = true
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true) @Published var isMoreMenuBarButtonItemHidden: Bool = true
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true) @Published var isMeBarButtonItemsHidden: Bool = true
@Published var isPagingEnabled = true
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false) // @Published var protected: Bool? = nil
let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true) // let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
self.context = context self.context = context
self.user = mastodonUser self.user = mastodonUser
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain) self.postsUserTimelineViewModel = UserTimelineViewModel(
self.userID = CurrentValueSubject(mastodonUser?.id) context: context,
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL()) title: L10n.Scene.Profile.SegmentedControl.posts,
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL()) queryFilter: .init(excludeReplies: true)
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback) )
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain) self.repliesUserTimelineViewModel = UserTimelineViewModel(
self.bioDescription = CurrentValueSubject(mastodonUser?.note) context: context,
self.url = CurrentValueSubject(mastodonUser?.url) title: L10n.Scene.Profile.SegmentedControl.postsAndReplies,
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) }) queryFilter: .init(excludeReplies: true)
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) }) )
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) }) self.mediaUserTimelineViewModel = UserTimelineViewModel(
self.protected = CurrentValueSubject(mastodonUser?.locked) context: context,
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) title: L10n.Scene.Profile.SegmentedControl.media,
self.fields = CurrentValueSubject(mastodonUser?.fields ?? []) queryFilter: .init(onlyMedia: true)
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:]) )
self.profileAboutViewModel = ProfileAboutViewModel(context: context)
super.init() super.init()
relationshipActionOptionSet // bind me
.compactMap { $0.highPriorityAction(except: []) }
.map { $0 == .none }
.assign(to: \.value, on: isRelationshipActionButtonHidden)
.store(in: &disposeBag)
// bind active authentication
context.authenticationService.activeMastodonAuthenticationBox context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] authenticationBox in .sink { [weak self] authenticationBox in
guard let self = self else { return } guard let self = self else { return }
guard let authenticationBox = authenticationBox else { self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
self.domain.value = nil
self.me = nil
return
}
self.domain.value = authenticationBox.domain
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
} }
.store(in: &disposeBag) .store(in: &disposeBag)
$me
.assign(to: \.me, on: relationshipViewModel)
.store(in: &disposeBag)
// bind user
$user
.map { user -> UserIdentifier? in
guard let user = user else { return nil }
return MastodonUserIdentifier(domain: user.domain, userID: user.id)
}
.assign(to: &$userIdentifier)
$user
.assign(to: \.user, on: relationshipViewModel)
.store(in: &disposeBag)
// bind userIdentifier
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
$userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
// bind bar button items
relationshipViewModel.$optionSet
.sink { [weak self] optionSet in
guard let self = self else { return }
guard let optionSet = optionSet, !optionSet.contains(.none) else {
self.isReplyBarButtonItemHidden = true
self.isMoreMenuBarButtonItemHidden = true
self.isMeBarButtonItemsHidden = true
return
}
let isMyself = optionSet.contains(.isMyself)
self.isReplyBarButtonItemHidden = isMyself
self.isMoreMenuBarButtonItemHidden = isMyself
self.isMeBarButtonItemsHidden = !isMyself
}
.store(in: &disposeBag)
// query relationship // query relationship
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) } user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
} }
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1) let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
// observe friendship // observe friendship
Publishers.CombineLatest3( Publishers.CombineLatest3(
userRecord, userRecord,
@ -148,200 +160,25 @@ class ProfileViewModel: NSObject {
} catch { } catch {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)") self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
} }
} } // end Task
} }
.store(in: &disposeBag) .store(in: &disposeBag)
//
let isBlockingOrBlocked = Publishers.CombineLatest( let isBlockingOrBlocked = Publishers.CombineLatest(
isBlocking, relationshipViewModel.$isBlocking,
isBlockedBy relationshipViewModel.$isBlockingBy
) )
.map { $0 || $1 } .map { $0 || $1 }
.share() .share()
isBlockingOrBlocked
.map { !$0 }
.assign(to: \.value, on: needsPagingEnabled)
.store(in: &disposeBag)
isBlockingOrBlocked
.map { $0 }
.assign(to: \.value, on: needsImageOverlayBlurred)
.store(in: &disposeBag)
setup()
}
}
extension ProfileViewModel {
private func setup() {
Publishers.CombineLatest(
$user,
$me
)
.receive(on: DispatchQueue.main)
.sink { [weak self] user, me in
guard let self = self else { return }
// Update view model attribute
self.update(mastodonUser: user)
self.update(mastodonUser: user, currentMastodonUser: me)
// Setup observer for user
if let mastodonUser = user {
// setup observer
self.mastodonUserObserver = ManagedObjectObserver.observe(object: mastodonUser)
.sink { completion in
switch completion {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .finished:
assertionFailure()
}
} receiveValue: { [weak self] change in
guard let self = self else { return }
guard let changeType = change.changeType else { return }
switch changeType {
case .update:
self.update(mastodonUser: mastodonUser)
self.update(mastodonUser: mastodonUser, currentMastodonUser: me)
case .delete:
// TODO:
break
}
}
} else {
self.mastodonUserObserver = nil
}
// Setup observer for user
if let currentMastodonUser = me {
// setup observer
self.currentMastodonUserObserver = ManagedObjectObserver.observe(object: currentMastodonUser)
.sink { completion in
switch completion {
case .failure(let error):
assertionFailure(error.localizedDescription)
case .finished:
assertionFailure()
}
} receiveValue: { [weak self] change in
guard let self = self else { return }
guard let changeType = change.changeType else { return }
switch changeType {
case .update:
self.update(mastodonUser: user, currentMastodonUser: currentMastodonUser)
case .delete:
// TODO:
break
}
}
} else {
self.currentMastodonUserObserver = nil
}
}
.store(in: &disposeBag)
}
private func update(mastodonUser: MastodonUser?) {
self.userID.value = mastodonUser?.id
self.bannerImageURL.value = mastodonUser?.headerImageURL()
self.avatarImageURL.value = mastodonUser?.avatarImageURL()
self.name.value = mastodonUser?.displayNameWithFallback
self.username.value = mastodonUser?.acctWithDomain
self.bioDescription.value = mastodonUser?.note
self.url.value = mastodonUser?.url
self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
self.protected.value = mastodonUser?.locked
self.suspended.value = mastodonUser?.suspended ?? false
self.fields.value = mastodonUser?.fields ?? []
self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
}
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
guard let mastodonUser = mastodonUser,
let currentMastodonUser = currentMastodonUser else {
// set relationship
self.relationshipActionOptionSet.value = .none
self.isFollowedBy.value = false
self.isMuting.value = false
self.isBlocking.value = false
self.isBlockedBy.value = false
// set bar button item state
self.isReplyBarButtonItemHidden.value = true
self.isMoreMenuBarButtonItemHidden.value = true
self.isMeBarButtonItemsHidden.value = true
return
}
if mastodonUser == currentMastodonUser { Publishers.CombineLatest(
self.relationshipActionOptionSet.value = [.edit] isBlockingOrBlocked,
// set bar button item state $isEditing
self.isReplyBarButtonItemHidden.value = true )
self.isMoreMenuBarButtonItemHidden.value = true .map { !$0 && !$1 }
self.isMeBarButtonItemsHidden.value = false .assign(to: &$isPagingEnabled)
} else {
// set with follow action default
var relationshipActionSet = RelationshipActionOptionSet([.follow])
if mastodonUser.locked {
relationshipActionSet.insert(.request)
}
if mastodonUser.suspended {
relationshipActionSet.insert(.suspended)
}
let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
if isFollowing {
relationshipActionSet.insert(.following)
}
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description)
let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser)
if isPending {
relationshipActionSet.insert(.pending)
}
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description)
let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser)
self.isFollowedBy.value = isFollowedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description)
let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser)
if isMuting {
relationshipActionSet.insert(.muting)
}
self.isMuting.value = isMuting
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description)
let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser)
if isBlocking {
relationshipActionSet.insert(.blocking)
}
self.isBlocking.value = isBlocking
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description)
let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser)
if isBlockedBy {
relationshipActionSet.insert(.blocked)
}
self.isBlockedBy.value = isBlockedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description)
self.relationshipActionOptionSet.value = relationshipActionSet
// set bar button item state
self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
self.isMoreMenuBarButtonItemHidden.value = false
self.isMeBarButtonItemsHidden.value = true
}
} }
} }
extension ProfileViewModel { extension ProfileViewModel {
@ -386,7 +223,7 @@ extension ProfileViewModel {
let authorization = authenticationBox.userAuthorization let authorization = authenticationBox.userAuthorization
let _image: UIImage? = { let _image: UIImage? = {
guard let image = headerProfileInfo.avatarImage else { return nil } guard let image = headerProfileInfo.avatar else { return nil }
guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else { guard image.size.width <= ProfileHeaderViewModel.avatarImageMaxSizeInPixel.width else {
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel) return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
} }

View File

@ -1,90 +0,0 @@
//
// ProfilePagingViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-29.
//
import os.log
import UIKit
import Pageboy
import Tabman
protocol ProfilePagingViewControllerDelegate: AnyObject {
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
}
final class ProfilePagingViewController: TabmanViewController {
weak var pagingDelegate: ProfilePagingViewControllerDelegate?
var viewModel: ProfilePagingViewModel!
// MARK: - PageboyViewControllerDelegate
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) {
super.pageboyViewController(pageboyViewController, didCancelScrollToPageAt: index, returnToPageAt: previousIndex)
// Fix the SDK bug for table view get row selected during swipe but cancel paging
guard previousIndex < viewModel.viewControllers.count else { return }
let viewController = viewModel.viewControllers[previousIndex]
if let tableView = viewController.scrollView as? UITableView {
for cell in tableView.visibleCells {
cell.setHighlighted(false, animated: false)
}
}
}
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) {
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated)
let viewController = viewModel.viewControllers[index]
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index)
}
// make key commands works
override var canBecomeFirstResponder: Bool {
return true
}
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ProfilePagingViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
dataSource = viewModel
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
}
// workaround to fix tab man responder chain issue
extension ProfilePagingViewController {
override var keyCommands: [UIKeyCommand]? {
return currentViewController?.keyCommands
}
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
}
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
}
}

View File

@ -1,82 +0,0 @@
//
// ProfilePagingViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-29.
//
import os.log
import UIKit
import Pageboy
import Tabman
import MastodonAsset
import MastodonLocalization
final class ProfilePagingViewModel: NSObject {
let postUserTimelineViewController = UserTimelineViewController()
let repliesUserTimelineViewController = UserTimelineViewController()
let mediaUserTimelineViewController = UserTimelineViewController()
let profileAboutViewController = ProfileAboutViewController()
init(
postsUserTimelineViewModel: UserTimelineViewModel,
repliesUserTimelineViewModel: UserTimelineViewModel,
mediaUserTimelineViewModel: UserTimelineViewModel,
profileAboutViewModel: ProfileAboutViewModel
) {
postUserTimelineViewController.viewModel = postsUserTimelineViewModel
repliesUserTimelineViewController.viewModel = repliesUserTimelineViewModel
mediaUserTimelineViewController.viewModel = mediaUserTimelineViewModel
profileAboutViewController.viewModel = profileAboutViewModel
super.init()
}
var viewControllers: [ScrollViewContainer] {
return [
postUserTimelineViewController,
repliesUserTimelineViewController,
mediaUserTimelineViewController,
profileAboutViewController,
]
}
let barItems: [TMBarItemable] = {
let items = [
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
]
return items
}()
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
// MARK: - PageboyViewControllerDataSource
extension ProfilePagingViewModel: PageboyViewControllerDataSource {
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
return viewControllers.count
}
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
return viewControllers[index]
}
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
return .first
}
}
// MARK: - TMBarDataSource
extension ProfilePagingViewModel: TMBarDataSource {
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
return barItems[index]
}
}

View File

@ -1,39 +0,0 @@
//
// ProfileSegmentedViewController.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-29.
//
import os.log
import UIKit
final class ProfileSegmentedViewController: UIViewController {
let pagingViewController = ProfilePagingViewController()
deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ProfileSegmentedViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
addChild(pagingViewController)
pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pagingViewController.view)
pagingViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
pagingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: pagingViewController.view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: pagingViewController.view.bottomAnchor),
])
}
}

View File

@ -11,6 +11,8 @@ import AVKit
import Combine import Combine
import CoreDataStack import CoreDataStack
import GameplayKit import GameplayKit
import TabBarPager
import XLPagerTabStrip
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -143,7 +145,14 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView
// MARK: - CustomScrollViewContainerController // MARK: - CustomScrollViewContainerController
extension UserTimelineViewController: ScrollViewContainer { extension UserTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView } var scrollView: UIScrollView { return tableView }
}
// MARK: - TabBarPage
extension UserTimelineViewController: TabBarPage {
var pageScrollView: UIScrollView {
scrollView
}
} }
// MARK: - StatusTableViewCellDelegate // MARK: - StatusTableViewCellDelegate
@ -165,3 +174,10 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender) statusKeyCommandHandler(sender)
} }
} }
// MARK: - IndicatorInfoProvider
extension UserTimelineViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: viewModel.title)
}
}

View File

@ -30,22 +30,19 @@ extension UserTimelineViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot) diffableDataSource?.apply(snapshot)
// trigger user timeline loading // trigger timeline reloading
Publishers.CombineLatest( $userIdentifier
$domain.removeDuplicates(), .receive(on: DispatchQueue.main)
$userID.removeDuplicates() .sink { [weak self] _ in
) guard let self = self else { return }
.receive(on: DispatchQueue.main) self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
.sink { [weak self] _ in }
guard let self = self else { return } .store(in: &disposeBag)
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
.store(in: &disposeBag)
let needsTimelineHidden = Publishers.CombineLatest3( let needsTimelineHidden = Publishers.CombineLatest3(
isBlocking, $isBlocking,
isBlockedBy, $isBlockedBy,
isSuspended $isSuspended
).map { $0 || $1 || $2 } ).map { $0 || $1 || $2 }
Publishers.CombineLatest( Publishers.CombineLatest(

View File

@ -50,7 +50,7 @@ extension UserTimelineViewModel.State {
guard let viewModel = viewModel else { return false } guard let viewModel = viewModel else { return false }
switch stateClass { switch stateClass {
case is Reloading.Type: case is Reloading.Type:
return viewModel.userID != nil return viewModel.userIdentifier != nil
default: default:
return false return false
} }
@ -132,7 +132,7 @@ extension UserTimelineViewModel.State {
let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last
guard let userID = viewModel.userID, !userID.isEmpty else { guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self) stateMachine.enter(Fail.self)
return return
} }
@ -194,7 +194,7 @@ extension UserTimelineViewModel.State {
guard let viewModel = viewModel, let _ = stateMachine else { return } guard let viewModel = viewModel, let _ = stateMachine else { return }
// trigger data source update. otherwise, spinner always display // trigger data source update. otherwise, spinner always display
viewModel.isSuspended.value = viewModel.isSuspended.value viewModel.isSuspended = viewModel.isSuspended
// remove bottom loader // remove bottom loader
guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return }

View File

@ -19,17 +19,18 @@ final class UserTimelineViewModel {
// input // input
let context: AppContext let context: AppContext
@Published var domain: String? let title: String
@Published var userID: String?
@Published var queryFilter: QueryFilter
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@Published var userIdentifier: UserIdentifier?
@Published var queryFilter: QueryFilter
let isBlocking = CurrentValueSubject<Bool, Never>(false) @Published var isBlocking = false
let isBlockedBy = CurrentValueSubject<Bool, Never>(false) @Published var isBlockedBy = false
let isSuspended = CurrentValueSubject<Bool, Never>(false) @Published var isSuspended = false
let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
var dataSourceDidUpdate = PassthroughSubject<Void, Never>() // let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
// var dataSourceDidUpdate = PassthroughSubject<Void, Never>()
// output // output
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>? var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
@ -48,30 +49,27 @@ final class UserTimelineViewModel {
init( init(
context: AppContext, context: AppContext,
domain: String?, title: String,
userID: String?,
queryFilter: QueryFilter queryFilter: QueryFilter
) { ) {
self.context = context self.context = context
self.title = title
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: nil,
additionalTweetPredicate: Status.notDeleted() additionalTweetPredicate: nil
) )
self.domain = domain
self.userID = userID
self.queryFilter = queryFilter self.queryFilter = queryFilter
// super.init() // super.init()
$domain context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.value, on: statusFetchedResultsController.domain) .assign(to: \.value, on: statusFetchedResultsController.domain)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
deinit { deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
} }
} }
@ -92,5 +90,4 @@ extension UserTimelineViewModel {
self.onlyMedia = onlyMedia self.onlyMedia = onlyMedia
} }
} }
} }

View File

@ -7,12 +7,12 @@
import UIKit import UIKit
// Make status bar style adaptive for child view controller
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
class AdaptiveStatusBarStyleNavigationController: UINavigationController { class AdaptiveStatusBarStyleNavigationController: UINavigationController {
private lazy var fullWidthBackGestureRecognizer = UIPanGestureRecognizer() private lazy var fullWidthBackGestureRecognizer = UIPanGestureRecognizer()
// Make status bar style adaptive for child view controller
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
override var childForStatusBarStyle: UIViewController? { override var childForStatusBarStyle: UIViewController? {
visibleViewController visibleViewController
} }

View File

@ -101,7 +101,9 @@ extension PollOptionView {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] theme in .sink { [weak self] theme in
guard let self = self else { return } guard let self = self else { return }
self.checkmarkBackgroundView.backgroundColor = theme.tertiarySystemBackgroundColor self.checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : theme.tableViewCellSelectionBackgroundColor
})
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }

View File

@ -23,6 +23,28 @@ extension APIService {
let query = Mastodon.API.Notifications.Query( let query = Mastodon.API.Notifications.Query(
maxID: maxID, maxID: maxID,
types: {
switch scope {
case .everything:
return [
.follow,
.followRequest,
.mention,
.reblog,
.favourite,
.poll,
.status,
]
case .mentions:
return [
.follow,
.followRequest,
.reblog,
.favourite,
.poll
]
}
}(),
excludeTypes: { excludeTypes: {
switch scope { switch scope {
case .everything: case .everything:

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionAttributes</key> <key>NSExtensionAttributes</key>

View File

@ -27,7 +27,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
.package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"), .package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"),
.package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"), .package(url: "https://github.com/Flipboard/FLAnimatedImage.git", from: "1.0.0"),
.package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.3")), .package(url: "https://github.com/TwidereProject/MetaTextKit.git", .exact("2.2.5")),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0"), .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/Alamofire/AlamofireImage.git", from: "4.1.0"),
.package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"), .package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"),

View File

@ -305,12 +305,12 @@ téléversé sur Mastodon.";
"Scene.Report.StepFinal.BlockUser" = "Bloquer %@"; "Scene.Report.StepFinal.BlockUser" = "Bloquer %@";
"Scene.Report.StepFinal.DontWantToSeeThis" = "Vous ne voulez pas voir cela ?"; "Scene.Report.StepFinal.DontWantToSeeThis" = "Vous ne voulez pas voir cela ?";
"Scene.Report.StepFinal.MuteUser" = "Masquer %@"; "Scene.Report.StepFinal.MuteUser" = "Masquer %@";
"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if theyve been blocked."; "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Ils ne seront plus en mesure de suivre ou de voir vos messages, mais iels peuvent voir siels ont été bloqué·e·s.";
"Scene.Report.StepFinal.Unfollow" = "Se désabonner"; "Scene.Report.StepFinal.Unfollow" = "Se désabonner";
"Scene.Report.StepFinal.UnfollowUser" = "Ne plus suivre %@"; "Scene.Report.StepFinal.UnfollowUser" = "Ne plus suivre %@";
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed"; "Scene.Report.StepFinal.Unfollowed" = "Unfollowed";
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Quand vous voyez quelque chose que vous naimez pas sur Mastodon, vous pouvez retirer la personne de votre expérience."; "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Quand vous voyez quelque chose que vous naimez pas sur Mastodon, vous pouvez retirer la personne de votre expérience.";
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@"; "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Pendant que nous étudions votre requête, vous pouvez prendre des mesures contre %@";
"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas quiels ont été mis en sourdine."; "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Vous ne verrez plus leurs messages ou leurs partages dans votre flux personnel. Iels ne sauront pas quiels ont été mis en sourdine.";
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Y a-t-il autre chose que nous devrions savoir ?"; "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Y a-t-il autre chose que nous devrions savoir ?";
"Scene.Report.StepFour.Step4Of4" = "Étape 4 sur 4"; "Scene.Report.StepFour.Step4Of4" = "Étape 4 sur 4";
@ -320,7 +320,7 @@ téléversé sur Mastodon.";
"Scene.Report.StepOne.ItsSomethingElse" = "Pour une autre raison"; "Scene.Report.StepOne.ItsSomethingElse" = "Pour une autre raison";
"Scene.Report.StepOne.ItsSpam" = "Cest du spam"; "Scene.Report.StepOne.ItsSpam" = "Cest du spam";
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Liens malveillants, engagement mensonger ou réponses répétitives"; "Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Liens malveillants, engagement mensonger ou réponses répétitives";
"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match"; "Scene.Report.StepOne.SelectTheBestMatch" = "Sélectionnez ce qui correspond le mieux";
"Scene.Report.StepOne.Step1Of4" = "Étape 1 sur 4"; "Scene.Report.StepOne.Step1Of4" = "Étape 1 sur 4";
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Le problème ne correspond à aucune des catégories"; "Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Le problème ne correspond à aucune des catégories";
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Quest-ce qui ne va pas avec ce compte ?"; "Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Quest-ce qui ne va pas avec ce compte ?";
@ -374,7 +374,7 @@ téléversé sur Mastodon.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Recherche des serveurs disponibles..."; "Scene.ServerPicker.EmptyState.FindingServers" = "Recherche des serveurs disponibles...";
"Scene.ServerPicker.EmptyState.NoResults" = "Aucun résultat"; "Scene.ServerPicker.EmptyState.NoResults" = "Aucun résultat";
"Scene.ServerPicker.Input.Placeholder" = "Trouvez un serveur ou rejoignez le vôtre..."; "Scene.ServerPicker.Input.Placeholder" = "Trouvez un serveur ou rejoignez le vôtre...";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Rechercher des serveurs ou entrer une URL";
"Scene.ServerPicker.Label.Category" = "CATÉGORIE"; "Scene.ServerPicker.Label.Category" = "CATÉGORIE";
"Scene.ServerPicker.Label.Language" = "LANGUE"; "Scene.ServerPicker.Label.Language" = "LANGUE";
"Scene.ServerPicker.Label.Users" = "UTILISATEUR·RICE·S"; "Scene.ServerPicker.Label.Users" = "UTILISATEUR·RICE·S";

View File

@ -208,14 +208,14 @@ ser subido a Mastodon.";
"Scene.Discovery.Tabs.Hashtags" = "Cancelos"; "Scene.Discovery.Tabs.Hashtags" = "Cancelos";
"Scene.Discovery.Tabs.News" = "Novas"; "Scene.Discovery.Tabs.News" = "Novas";
"Scene.Discovery.Tabs.Posts" = "Publicacións"; "Scene.Discovery.Tabs.Posts" = "Publicacións";
"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@"; "Scene.Familiarfollowers.FollowedByNames" = "Seguimentos de %@";
"Scene.Familiarfollowers.Title" = "Followers you familiar"; "Scene.Familiarfollowers.Title" = "Seguimentos próximos";
"Scene.Favorite.Title" = "Publicacións Favoritas"; "Scene.Favorite.Title" = "Publicacións Favoritas";
"Scene.FavoritedBy.Title" = "Favorited By"; "Scene.FavoritedBy.Title" = "Favorecido por";
"Scene.Follower.Footer" = "Non se mostran seguidoras desde outros servidores."; "Scene.Follower.Footer" = "Non se mostran seguidoras desde outros servidores.";
"Scene.Follower.Title" = "follower"; "Scene.Follower.Title" = "seguidora";
"Scene.Following.Footer" = "Non se mostran os seguimentos desde outros servidores."; "Scene.Following.Footer" = "Non se mostran os seguimentos desde outros servidores.";
"Scene.Following.Title" = "following"; "Scene.Following.Title" = "seguindo";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Toca para ir arriba e toca outra vez para volver ao mesmo lugar"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Toca para ir arriba e toca outra vez para volver ao mesmo lugar";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Botón do logo"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Botón do logo";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Novas publicacións"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Novas publicacións";
@ -259,7 +259,7 @@ ser subido a Mastodon.";
"Scene.Profile.SegmentedControl.Posts" = "Publicacións"; "Scene.Profile.SegmentedControl.Posts" = "Publicacións";
"Scene.Profile.SegmentedControl.PostsAndReplies" = "Publicacións e respostas"; "Scene.Profile.SegmentedControl.PostsAndReplies" = "Publicacións e respostas";
"Scene.Profile.SegmentedControl.Replies" = "Respostas"; "Scene.Profile.SegmentedControl.Replies" = "Respostas";
"Scene.RebloggedBy.Title" = "Reblogged By"; "Scene.RebloggedBy.Title" = "Promovido por";
"Scene.Register.Error.Item.Agreement" = "Acordo"; "Scene.Register.Error.Item.Agreement" = "Acordo";
"Scene.Register.Error.Item.Email" = "Email"; "Scene.Register.Error.Item.Email" = "Email";
"Scene.Register.Error.Item.Locale" = "Locale"; "Scene.Register.Error.Item.Locale" = "Locale";
@ -374,7 +374,7 @@ ser subido a Mastodon.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Buscando servidores dispoñibles..."; "Scene.ServerPicker.EmptyState.FindingServers" = "Buscando servidores dispoñibles...";
"Scene.ServerPicker.EmptyState.NoResults" = "Sen resultados"; "Scene.ServerPicker.EmptyState.NoResults" = "Sen resultados";
"Scene.ServerPicker.Input.Placeholder" = "Buscar comunidades"; "Scene.ServerPicker.Input.Placeholder" = "Buscar comunidades";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Busca un servidor ou escribe URL";
"Scene.ServerPicker.Label.Category" = "CATEGORÍA"; "Scene.ServerPicker.Label.Category" = "CATEGORÍA";
"Scene.ServerPicker.Label.Language" = "IDIOMA"; "Scene.ServerPicker.Label.Language" = "IDIOMA";
"Scene.ServerPicker.Label.Users" = "USUARIAS"; "Scene.ServerPicker.Label.Users" = "USUARIAS";

View File

@ -118,8 +118,8 @@ Per favore verifica la tua connessione internet.";
"Common.Controls.Status.Tag.Mention" = "Menzione"; "Common.Controls.Status.Tag.Mention" = "Menzione";
"Common.Controls.Status.Tag.Url" = "URL"; "Common.Controls.Status.Tag.Url" = "URL";
"Common.Controls.Status.TapToReveal" = "Tocca per rivelare"; "Common.Controls.Status.TapToReveal" = "Tocca per rivelare";
"Common.Controls.Status.UserReblogged" = "%@ hanno condiviso"; "Common.Controls.Status.UserReblogged" = "%@ ha condiviso";
"Common.Controls.Status.UserRepliedTo" = "Rispondi a %@"; "Common.Controls.Status.UserRepliedTo" = "Risposta a %@";
"Common.Controls.Status.Visibility.Direct" = "Solo l'utente menzionato può vedere questo post."; "Common.Controls.Status.Visibility.Direct" = "Solo l'utente menzionato può vedere questo post.";
"Common.Controls.Status.Visibility.Private" = "Solo i loro seguaci possono vedere questo post."; "Common.Controls.Status.Visibility.Private" = "Solo i loro seguaci possono vedere questo post.";
"Common.Controls.Status.Visibility.PrivateFromMe" = "Solo i miei seguaci possono vedere questo post."; "Common.Controls.Status.Visibility.PrivateFromMe" = "Solo i miei seguaci possono vedere questo post.";
@ -218,7 +218,7 @@ caricato su Mastodon.";
"Scene.Following.Title" = "seguendo"; "Scene.Following.Title" = "seguendo";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tocca per scorrere verso l'alto e tocca di nuovo verso la posizione precedente"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tocca per scorrere verso l'alto e tocca di nuovo verso la posizione precedente";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Pulsante Logo"; "Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Pulsante Logo";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Vedi nuovi post"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Vedi i nuovi post";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Non in linea"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Non in linea";
"Scene.HomeTimeline.NavigationBarState.Published" = "Pubblicato!"; "Scene.HomeTimeline.NavigationBarState.Published" = "Pubblicato!";
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Pubblicazione post..."; "Scene.HomeTimeline.NavigationBarState.Publishing" = "Pubblicazione post...";
@ -229,7 +229,7 @@ caricato su Mastodon.";
"Scene.Notification.NotificationDescription.FollowedYou" = "ti ha seguito"; "Scene.Notification.NotificationDescription.FollowedYou" = "ti ha seguito";
"Scene.Notification.NotificationDescription.MentionedYou" = "ti ha menzionato"; "Scene.Notification.NotificationDescription.MentionedYou" = "ti ha menzionato";
"Scene.Notification.NotificationDescription.PollHasEnded" = "sondaggio terminato"; "Scene.Notification.NotificationDescription.PollHasEnded" = "sondaggio terminato";
"Scene.Notification.NotificationDescription.RebloggedYourPost" = "ha ripostato il tuo post"; "Scene.Notification.NotificationDescription.RebloggedYourPost" = "ha condiviso il tuo post";
"Scene.Notification.NotificationDescription.RequestToFollowYou" = "richiesta di seguirti"; "Scene.Notification.NotificationDescription.RequestToFollowYou" = "richiesta di seguirti";
"Scene.Notification.Title.Everything" = "Tutto"; "Scene.Notification.Title.Everything" = "Tutto";
"Scene.Notification.Title.Mentions" = "Menzioni"; "Scene.Notification.Title.Mentions" = "Menzioni";

View File

@ -297,38 +297,38 @@
"Scene.Report.SkipToSend" = "コメントなしで送信"; "Scene.Report.SkipToSend" = "コメントなしで送信";
"Scene.Report.Step1" = "ステップ 1/2"; "Scene.Report.Step1" = "ステップ 1/2";
"Scene.Report.Step2" = "ステップ 2/2"; "Scene.Report.Step2" = "ステップ 2/2";
"Scene.Report.StepFinal.BlockUser" = "Block %@"; "Scene.Report.StepFinal.BlockUser" = "%@をブロック";
"Scene.Report.StepFinal.DontWantToSeeThis" = "Dont want to see this?"; "Scene.Report.StepFinal.DontWantToSeeThis" = "Dont want to see this?";
"Scene.Report.StepFinal.MuteUser" = "Mute %@"; "Scene.Report.StepFinal.MuteUser" = "%@をミュート";
"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if theyve been blocked."; "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if theyve been blocked.";
"Scene.Report.StepFinal.Unfollow" = "Unfollow"; "Scene.Report.StepFinal.Unfollow" = "フォロー解除";
"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@"; "Scene.Report.StepFinal.UnfollowUser" = "%@をフォロー解除";
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed"; "Scene.Report.StepFinal.Unfollowed" = "フォロー解除しました";
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you dont like on Mastodon, you can remove the person from your experience."; "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you dont 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.WhileWeReviewThisYouCanTakeActionAgainstUser" = "私たちが確認している間でも、あなたは%@さんに対して対応することができます。";
"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You wont see their posts or reblogs in your home feed. They wont know theyve been muted."; "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You wont see their posts or reblogs in your home feed. They wont know theyve been muted.";
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?"; "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "その他に私たちに伝えておくべき事はありますか?";
"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4"; "Scene.Report.StepFour.Step4Of4" = "ステップ 4/4";
"Scene.Report.StepOne.IDontLikeIt" = "I dont like it"; "Scene.Report.StepOne.IDontLikeIt" = "興味がありません";
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see"; "Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "見たくない内容の場合";
"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules"; "Scene.Report.StepOne.ItViolatesServerRules" = "サーバーのルールに違反しています";
"Scene.Report.StepOne.ItsSomethingElse" = "Its something else"; "Scene.Report.StepOne.ItsSomethingElse" = "その他";
"Scene.Report.StepOne.ItsSpam" = "Its spam"; "Scene.Report.StepOne.ItsSpam" = "これはスパムです";
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies"; "Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "悪意あるリンクや虚偽の情報、執拗な返信など";
"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match"; "Scene.Report.StepOne.SelectTheBestMatch" = "最も近いものを選んでください";
"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4"; "Scene.Report.StepOne.Step1Of4" = "ステップ 1/4";
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories"; "Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "当てはまる選択肢がない場合";
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?"; "Scene.Report.StepOne.WhatsWrongWithThisAccount" = "このアカウントのどこが問題ですか?";
"Scene.Report.StepOne.WhatsWrongWithThisPost" = "What's wrong with this post?"; "Scene.Report.StepOne.WhatsWrongWithThisPost" = "この投稿のどこが問題ですか?";
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?"; "Scene.Report.StepOne.WhatsWrongWithThisUsername" = "%@さんのどこが問題ですか?";
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules"; "Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "ルールに違反しているのを見つけた場合";
"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?"; "Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?";
"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply"; "Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply";
"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4"; "Scene.Report.StepThree.Step3Of4" = "ステップ 3/4";
"Scene.Report.StepTwo.IJustDontLikeIt" = "I just dont like it"; "Scene.Report.StepTwo.IJustDontLikeIt" = "I just dont like it";
"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply"; "Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply";
"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4"; "Scene.Report.StepTwo.Step2Of4" = "ステップ 2/4";
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?"; "Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "どのルールに違反していますか?";
"Scene.Report.TextPlaceholder" = "追加コメントを入力"; "Scene.Report.TextPlaceholder" = "追加コメントを入力";
"Scene.Report.Title" = "%@を通報"; "Scene.Report.Title" = "%@を通報";
"Scene.Report.TitleReport" = "報告する"; "Scene.Report.TitleReport" = "報告する";
@ -369,7 +369,7 @@
"Scene.ServerPicker.EmptyState.FindingServers" = "利用可能なサーバーの検索..."; "Scene.ServerPicker.EmptyState.FindingServers" = "利用可能なサーバーの検索...";
"Scene.ServerPicker.EmptyState.NoResults" = "なし"; "Scene.ServerPicker.EmptyState.NoResults" = "なし";
"Scene.ServerPicker.Input.Placeholder" = "サーバーを探す"; "Scene.ServerPicker.Input.Placeholder" = "サーバーを探す";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "サーバーを検索またはURLを入力";
"Scene.ServerPicker.Label.Category" = "カテゴリー"; "Scene.ServerPicker.Label.Category" = "カテゴリー";
"Scene.ServerPicker.Label.Language" = "言語"; "Scene.ServerPicker.Label.Language" = "言語";
"Scene.ServerPicker.Label.Users" = "ユーザー"; "Scene.ServerPicker.Label.Users" = "ユーザー";

View File

@ -200,10 +200,10 @@ Ad d-yettwasali ɣef Mastodon.";
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Ldi amsaɣ n yimayl"; "Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Ldi amsaɣ n yimayl";
"Scene.ConfirmEmail.OpenEmailApp.Title" = "Sefqed Tanaka-inek."; "Scene.ConfirmEmail.OpenEmailApp.Title" = "Sefqed Tanaka-inek.";
"Scene.ConfirmEmail.Subtitle" = "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik."; "Scene.ConfirmEmail.Subtitle" = "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik.";
"Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Tap the link we emailed to you to verify your account"; "Scene.ConfirmEmail.TapTheLinkWeEmailedToYouToVerifyYourAccount" = "Sit ɣef useɣwen i ak-n-uznen i wakken ad tesneqdeḍ amiḍan-ik";
"Scene.ConfirmEmail.Title" = "Taɣawsa taneggarut."; "Scene.ConfirmEmail.Title" = "Taɣawsa taneggarut.";
"Scene.Discovery.Intro" = "Tigi d tisuffaɣ i d-ijebbden s waṭas deg tama-inek•inem n Mastodon."; "Scene.Discovery.Intro" = "Tigi d tisuffaɣ i d-ijebbden s waṭas deg tama-inek•inem n Mastodon.";
"Scene.Discovery.Tabs.Community" = "Community"; "Scene.Discovery.Tabs.Community" = "Tamɣiwent";
"Scene.Discovery.Tabs.ForYou" = "I kečč·kem"; "Scene.Discovery.Tabs.ForYou" = "I kečč·kem";
"Scene.Discovery.Tabs.Hashtags" = "Ihacṭagen"; "Scene.Discovery.Tabs.Hashtags" = "Ihacṭagen";
"Scene.Discovery.Tabs.News" = "Isallen"; "Scene.Discovery.Tabs.News" = "Isallen";
@ -213,11 +213,11 @@ Ad d-yettwasali ɣef Mastodon.";
"Scene.Favorite.Title" = "Ismenyifen-ik·im"; "Scene.Favorite.Title" = "Ismenyifen-ik·im";
"Scene.FavoritedBy.Title" = "Favorited By"; "Scene.FavoritedBy.Title" = "Favorited By";
"Scene.Follower.Footer" = "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."; "Scene.Follower.Footer" = "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara.";
"Scene.Follower.Title" = "follower"; "Scene.Follower.Title" = "aneḍfar";
"Scene.Following.Footer" = "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."; "Scene.Following.Footer" = "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara.";
"Scene.Following.Title" = "following"; "Scene.Following.Title" = "yeṭṭafar";
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoHint" = "Tap to scroll to top and tap again to previous location"; "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.Accessibility.LogoLabel" = "Taqeffalt n ulugu";
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Tissufaɣ timaynutin"; "Scene.HomeTimeline.NavigationBarState.NewPosts" = "Tissufaɣ timaynutin";
"Scene.HomeTimeline.NavigationBarState.Offline" = "Beṛṛa n tuqqna"; "Scene.HomeTimeline.NavigationBarState.Offline" = "Beṛṛa n tuqqna";
"Scene.HomeTimeline.NavigationBarState.Published" = "Yettwasuffeɣ!"; "Scene.HomeTimeline.NavigationBarState.Published" = "Yettwasuffeɣ!";
@ -292,7 +292,7 @@ Ad d-yettwasali ɣef Mastodon.";
"Scene.Register.Input.Password.Require" = "Awal-ik uffir yesra ma drus:"; "Scene.Register.Input.Password.Require" = "Awal-ik uffir yesra ma drus:";
"Scene.Register.Input.Username.DuplicatePrompt" = "Isem-ayi n umseqdac yettwaṭṭef yakan."; "Scene.Register.Input.Username.DuplicatePrompt" = "Isem-ayi n umseqdac yettwaṭṭef yakan.";
"Scene.Register.Input.Username.Placeholder" = "isem n useqdac"; "Scene.Register.Input.Username.Placeholder" = "isem n useqdac";
"Scene.Register.LetsGetYouSetUpOnDomain" = "Lets get you set up on %@"; "Scene.Register.LetsGetYouSetUpOnDomain" = "Aha ad nebdu asbadu ɣef %@";
"Scene.Register.Title" = "Aha ad nebdu asbadu ɣef %@"; "Scene.Register.Title" = "Aha ad nebdu asbadu ɣef %@";
"Scene.Report.Content1" = "Tebɣiḍ ad ternuḍ tisuffaɣ-nniḍen ɣer uneqqis?"; "Scene.Report.Content1" = "Tebɣiḍ ad ternuḍ tisuffaɣ-nniḍen ɣer uneqqis?";
"Scene.Report.Content2" = "Yella wayen i ilaqen ad teẓren yimḍebbren ɣef uneqqis-a?"; "Scene.Report.Content2" = "Yella wayen i ilaqen ad teẓren yimḍebbren ɣef uneqqis-a?";
@ -302,38 +302,38 @@ Ad d-yettwasali ɣef Mastodon.";
"Scene.Report.SkipToSend" = "Azen s war awennit"; "Scene.Report.SkipToSend" = "Azen s war awennit";
"Scene.Report.Step1" = "Aḥric 1 seg 2"; "Scene.Report.Step1" = "Aḥric 1 seg 2";
"Scene.Report.Step2" = "Aḥric 2 seg 2"; "Scene.Report.Step2" = "Aḥric 2 seg 2";
"Scene.Report.StepFinal.BlockUser" = "Block %@"; "Scene.Report.StepFinal.BlockUser" = "Sewḥel %@";
"Scene.Report.StepFinal.DontWantToSeeThis" = "Dont want to see this?"; "Scene.Report.StepFinal.DontWantToSeeThis" = "Ur tebɣiḍ ara ad twaliḍ aya?";
"Scene.Report.StepFinal.MuteUser" = "Mute %@"; "Scene.Report.StepFinal.MuteUser" = "Sgugem %@";
"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if theyve been blocked."; "Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Ur ttuɣalen ara ad izmiren ad ak•akem-ḍefren neɣ ad walin tisuffaɣ-inek•inem, maca ad walin ma yella ttusweḥlen.";
"Scene.Report.StepFinal.Unfollow" = "Unfollow"; "Scene.Report.StepFinal.Unfollow" = "Ur ṭṭafaṛ ara";
"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@"; "Scene.Report.StepFinal.UnfollowUser" = "Y•Teḥbes aḍfar n %@";
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed"; "Scene.Report.StepFinal.Unfollowed" = "Y•Teḥbes aḍfar n";
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you dont like on Mastodon, you can remove the person from your experience."; "Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Mi ara twaliḍ kra ur ak•am-neɛǧib ara ɣef Mastodon, tzemreḍ ad tekkseḍ amdan-nni seg tirmit-ik•im.";
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@"; "Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "Ideg nekkni nessenqad tuttra-inek•inem, tzemreḍ ad tḥadreḍ mgal %@";
"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You wont see their posts or reblogs in your home feed. They wont know theyve been muted."; "Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "Ur tettwaliḍ ara tisuffaɣ-nsen neɣ iriblugen-nsen deg usuddem-inek•inem agejdan. Ur ẓerren ara belli tesgugmeḍ-ten.";
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?"; "Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Yella wayen-nniḍen i ilaqen ad t-nẓer?";
"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4"; "Scene.Report.StepFour.Step4Of4" = "Aḥric 4 seg 4";
"Scene.Report.StepOne.IDontLikeIt" = "I dont like it"; "Scene.Report.StepOne.IDontLikeIt" = "Ur ḥemmleɣ ara aya";
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see"; "Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "D ayen akk ur bɣiɣ ara ad waliɣ";
"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules"; "Scene.Report.StepOne.ItViolatesServerRules" = "Truẓi n yilugan n uqeddac";
"Scene.Report.StepOne.ItsSomethingElse" = "Its something else"; "Scene.Report.StepOne.ItsSomethingElse" = "Ɣef ssebba-nniḍen";
"Scene.Report.StepOne.ItsSpam" = "Its spam"; "Scene.Report.StepOne.ItsSpam" = "D aspam";
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies"; "Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Yir iseɣwan, yir agman d tririyin i d-yettuɣalen";
"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match"; "Scene.Report.StepOne.SelectTheBestMatch" = "Fren amṣada akk igerrzen";
"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4"; "Scene.Report.StepOne.Step1Of4" = "Aḥric 1 seg 4";
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories"; "Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "Ugur ur yemṣada ara akk d taggayin-nniḍen";
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?"; "Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?";
"Scene.Report.StepOne.WhatsWrongWithThisPost" = "What's wrong with this post?"; "Scene.Report.StepOne.WhatsWrongWithThisPost" = "Acu n wugur yellan d tsuffeɣt-a?";
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?"; "Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Acu n wugur yellan d %@?";
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules"; "Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Teẓriḍ y•tettruẓu kra n yilugan";
"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?"; "Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Llant tsuffaɣ ara isdemren aneqqis-a?";
"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply"; "Scene.Report.StepThree.SelectAllThatApply" = "Fren akk tifrat ara yettusnasen";
"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4"; "Scene.Report.StepThree.Step3Of4" = "Aḥric 3 seg 4";
"Scene.Report.StepTwo.IJustDontLikeIt" = "I just dont like it"; "Scene.Report.StepTwo.IJustDontLikeIt" = "Ur ḥemmleɣ ara kan aya";
"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply"; "Scene.Report.StepTwo.SelectAllThatApply" = "Fren akk tifrat ara yettusnasen";
"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4"; "Scene.Report.StepTwo.Step2Of4" = "Aḥric 2 seg 4";
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?"; "Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Acu n yilugan i yettwarẓan?";
"Scene.Report.TextPlaceholder" = "Aru neɣ senteḍ iwenniten-nniḍen"; "Scene.Report.TextPlaceholder" = "Aru neɣ senteḍ iwenniten-nniḍen";
"Scene.Report.Title" = "Aneqqis %@"; "Scene.Report.Title" = "Aneqqis %@";
"Scene.Report.TitleReport" = "Aneqqis"; "Scene.Report.TitleReport" = "Aneqqis";
@ -374,7 +374,7 @@ Ad d-yettwasali ɣef Mastodon.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Tifin n yiqeddacen yellan..."; "Scene.ServerPicker.EmptyState.FindingServers" = "Tifin n yiqeddacen yellan...";
"Scene.ServerPicker.EmptyState.NoResults" = "Ulac igemmaḍ"; "Scene.ServerPicker.EmptyState.NoResults" = "Ulac igemmaḍ";
"Scene.ServerPicker.Input.Placeholder" = "Nadi timɣiwnin"; "Scene.ServerPicker.Input.Placeholder" = "Nadi timɣiwnin";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Nadi timɣiwnin neɣ sekcem URL";
"Scene.ServerPicker.Label.Category" = "TAGGAYT"; "Scene.ServerPicker.Label.Category" = "TAGGAYT";
"Scene.ServerPicker.Label.Language" = "TUTLAYT"; "Scene.ServerPicker.Label.Language" = "TUTLAYT";
"Scene.ServerPicker.Label.Users" = "ISEQDACEN"; "Scene.ServerPicker.Label.Users" = "ISEQDACEN";

View File

@ -375,7 +375,7 @@ girêdanê bitikne da ku ajimêra xwe bidî piştrastkirin.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Peydakirina rajekarên berdest..."; "Scene.ServerPicker.EmptyState.FindingServers" = "Peydakirina rajekarên berdest...";
"Scene.ServerPicker.EmptyState.NoResults" = "Encam tune"; "Scene.ServerPicker.EmptyState.NoResults" = "Encam tune";
"Scene.ServerPicker.Input.Placeholder" = "Li rajekaran bigere"; "Scene.ServerPicker.Input.Placeholder" = "Li rajekaran bigere";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Li rajekaran bigere an jî girêdanê têxe";
"Scene.ServerPicker.Label.Category" = "BEŞ"; "Scene.ServerPicker.Label.Category" = "BEŞ";
"Scene.ServerPicker.Label.Language" = "ZIMAN"; "Scene.ServerPicker.Label.Language" = "ZIMAN";
"Scene.ServerPicker.Label.Users" = "BIKARHÊNER"; "Scene.ServerPicker.Label.Users" = "BIKARHÊNER";

View File

@ -374,7 +374,7 @@
"Scene.ServerPicker.EmptyState.FindingServers" = "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน..."; "Scene.ServerPicker.EmptyState.FindingServers" = "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...";
"Scene.ServerPicker.EmptyState.NoResults" = "ไม่มีผลลัพธ์"; "Scene.ServerPicker.EmptyState.NoResults" = "ไม่มีผลลัพธ์";
"Scene.ServerPicker.Input.Placeholder" = "ค้นหาเซิร์ฟเวอร์"; "Scene.ServerPicker.Input.Placeholder" = "ค้นหาเซิร์ฟเวอร์";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "ค้นหาเซิร์ฟเวอร์หรือป้อน URL";
"Scene.ServerPicker.Label.Category" = "หมวดหมู่"; "Scene.ServerPicker.Label.Category" = "หมวดหมู่";
"Scene.ServerPicker.Label.Language" = "ภาษา"; "Scene.ServerPicker.Label.Language" = "ภาษา";
"Scene.ServerPicker.Label.Users" = "ผู้ใช้"; "Scene.ServerPicker.Label.Users" = "ผู้ใช้";

View File

@ -374,7 +374,7 @@ tải lên Mastodon.";
"Scene.ServerPicker.EmptyState.FindingServers" = "Đang tìm máy chủ hoạt động..."; "Scene.ServerPicker.EmptyState.FindingServers" = "Đang tìm máy chủ hoạt động...";
"Scene.ServerPicker.EmptyState.NoResults" = "Không có kết quả"; "Scene.ServerPicker.EmptyState.NoResults" = "Không có kết quả";
"Scene.ServerPicker.Input.Placeholder" = "Tìm máy chủ"; "Scene.ServerPicker.Input.Placeholder" = "Tìm máy chủ";
"Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Search servers or enter URL"; "Scene.ServerPicker.Input.SearchServersOrEnterUrl" = "Tìm máy chủ hoặc nhập URL";
"Scene.ServerPicker.Label.Category" = "PHÂN LOẠI"; "Scene.ServerPicker.Label.Category" = "PHÂN LOẠI";
"Scene.ServerPicker.Label.Language" = "NGÔN NGỮ"; "Scene.ServerPicker.Label.Language" = "NGÔN NGỮ";
"Scene.ServerPicker.Label.Users" = "NGƯỜI DÙNG"; "Scene.ServerPicker.Label.Users" = "NGƯỜI DÙNG";

View File

@ -90,6 +90,7 @@ extension Mastodon.API.Notifications {
public let sinceID: Mastodon.Entity.Status.ID? public let sinceID: Mastodon.Entity.Status.ID?
public let minID: Mastodon.Entity.Status.ID? public let minID: Mastodon.Entity.Status.ID?
public let limit: Int? public let limit: Int?
public let types: [Mastodon.Entity.Notification.NotificationType]?
public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]? public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]?
public let accountID: String? public let accountID: String?
@ -98,6 +99,7 @@ extension Mastodon.API.Notifications {
sinceID: Mastodon.Entity.Status.ID? = nil, sinceID: Mastodon.Entity.Status.ID? = nil,
minID: Mastodon.Entity.Status.ID? = nil, minID: Mastodon.Entity.Status.ID? = nil,
limit: Int? = nil, limit: Int? = nil,
types: [Mastodon.Entity.Notification.NotificationType]? = nil,
excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil, excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil,
accountID: String? = nil accountID: String? = nil
) { ) {
@ -105,6 +107,7 @@ extension Mastodon.API.Notifications {
self.sinceID = sinceID self.sinceID = sinceID
self.minID = minID self.minID = minID
self.limit = limit self.limit = limit
self.types = types
self.excludeTypes = excludeTypes self.excludeTypes = excludeTypes
self.accountID = accountID self.accountID = accountID
} }
@ -115,6 +118,11 @@ extension Mastodon.API.Notifications {
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) } sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) } minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) }
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
if let types = types {
types.forEach {
items.append(URLQueryItem(name: "types[]", value: $0.rawValue))
}
}
if let excludeTypes = excludeTypes { if let excludeTypes = excludeTypes {
excludeTypes.forEach { excludeTypes.forEach {
items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue)) items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue))

View File

@ -0,0 +1,28 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-5-13.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}
public struct MastodonUserIdentifier: UserIdentifier {
public let domain: String
public var userID: Mastodon.Entity.Account.ID
public init(
domain: String,
userID: Mastodon.Entity.Account.ID
) {
self.domain = domain
self.userID = userID
}
}

View File

@ -1,14 +0,0 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-1-12.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}

View File

@ -84,7 +84,7 @@ public struct RelationshipActionOptionSet: OptionSet {
case .pending: return L10n.Common.Controls.Friendship.pending case .pending: return L10n.Common.Controls.Friendship.pending
case .following: return L10n.Common.Controls.Friendship.following case .following: return L10n.Common.Controls.Friendship.following
case .muting: return L10n.Common.Controls.Friendship.muted case .muting: return L10n.Common.Controls.Friendship.muted
case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user (deprecated)
case .blocking: return L10n.Common.Controls.Friendship.blocked case .blocking: return L10n.Common.Controls.Friendship.blocked
case .suspended: return L10n.Common.Controls.Friendship.follow case .suspended: return L10n.Common.Controls.Friendship.follow
case .edit: return L10n.Common.Controls.Friendship.editInfo case .edit: return L10n.Common.Controls.Friendship.editInfo
@ -116,6 +116,7 @@ public final class RelationshipViewModel {
@Published public var isMuting = false @Published public var isMuting = false
@Published public var isBlocking = false @Published public var isBlocking = false
@Published public var isBlockingBy = false @Published public var isBlockingBy = false
@Published public var isSuspended = false
public init() { public init() {
Publishers.CombineLatest3( Publishers.CombineLatest3(
@ -182,8 +183,8 @@ extension RelationshipViewModel {
self.isMuting = optionSet.contains(.muting) self.isMuting = optionSet.contains(.muting)
self.isBlockingBy = optionSet.contains(.blockingBy) self.isBlockingBy = optionSet.contains(.blockingBy)
self.isBlocking = optionSet.contains(.blocking) self.isBlocking = optionSet.contains(.blocking)
self.isSuspended = optionSet.contains(.suspended)
self.optionSet = optionSet self.optionSet = optionSet
} }
@ -203,7 +204,7 @@ extension RelationshipViewModel {
public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet { public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet {
let isMyself = user.id == me.id && user.domain == me.domain let isMyself = user.id == me.id && user.domain == me.domain
guard !isMyself else { guard !isMyself else {
return [.isMyself] return [.isMyself, .edit]
} }
let isProtected = user.locked let isProtected = user.locked
@ -247,6 +248,10 @@ extension RelationshipViewModel {
if isBlocking { if isBlocking {
optionSet.insert(.blocking) optionSet.insert(.blocking)
} }
if user.suspended {
optionSet.insert(.suspended)
}
return optionSet return optionSet
} }

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
</dict> </dict>
</plist> </plist>

View File

@ -15,8 +15,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
</dict> </dict>
</plist> </plist>

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionPointIdentifier</key> <key>NSExtensionPointIdentifier</key>

View File

@ -8,6 +8,7 @@ target 'Mastodon' do
# UI # UI
pod 'UITextField+Shake', '~> 1.2' pod 'UITextField+Shake', '~> 1.2'
pod 'XLPagerTabStrip', '~> 9.0.0'
# misc # misc
pod 'SwiftGen', '~> 6.4.0' pod 'SwiftGen', '~> 6.4.0'

View File

@ -8,6 +8,7 @@ PODS:
- Sourcery/CLI-Only (1.6.1) - Sourcery/CLI-Only (1.6.1)
- SwiftGen (6.4.0) - SwiftGen (6.4.0)
- "UITextField+Shake (1.2.1)" - "UITextField+Shake (1.2.1)"
- XLPagerTabStrip (9.0.0)
DEPENDENCIES: DEPENDENCIES:
- DateToolsSwift (~> 5.0.0) - DateToolsSwift (~> 5.0.0)
@ -17,6 +18,7 @@ DEPENDENCIES:
- Sourcery (~> 1.6.1) - Sourcery (~> 1.6.1)
- SwiftGen (~> 6.4.0) - SwiftGen (~> 6.4.0)
- "UITextField+Shake (~> 1.2)" - "UITextField+Shake (~> 1.2)"
- XLPagerTabStrip (~> 9.0.0)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@ -26,6 +28,7 @@ SPEC REPOS:
- Sourcery - Sourcery
- SwiftGen - SwiftGen
- "UITextField+Shake" - "UITextField+Shake"
- XLPagerTabStrip
EXTERNAL SOURCES: EXTERNAL SOURCES:
Keys: Keys:
@ -39,7 +42,8 @@ SPEC CHECKSUMS:
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
PODFILE CHECKSUM: 335d0ca70493d4c280d0f8fd7f26fe9be6a4e289 PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.2</string> <string>1.4.3</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>133</string> <string>138</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionAttributes</key> <key>NSExtensionAttributes</key>