commit
cdf6c44aaa
|
@ -15,8 +15,8 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
|
||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||
- [Tabman](https://github.com/uias/Tabman)
|
||||
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
|
||||
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
|
||||
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
|
||||
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController)
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Recherche des serveurs disponibles...",
|
||||
|
@ -621,7 +621,7 @@
|
|||
"whats_wrong_with_this_post": "Qu’est-ce qui ne va pas avec ce message ?",
|
||||
"whats_wrong_with_this_account": "Qu’est-ce qui ne va pas avec ce compte ?",
|
||||
"whats_wrong_with_this_username": "Qu’est-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 n’aime pas",
|
||||
"it_is_not_something_you_want_to_see": "C’est quelque chose que vous ne souhaitez pas voir",
|
||||
"its_spam": "C’est du spam",
|
||||
|
@ -655,8 +655,8 @@
|
|||
"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 qu’iels ont été mis en sourdine.",
|
||||
"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 they’ve been blocked.",
|
||||
"while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s"
|
||||
"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 s’iels ont été bloqué·e·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": {
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Buscando servidores dispoñibles...",
|
||||
|
@ -462,22 +462,22 @@
|
|||
}
|
||||
},
|
||||
"follower": {
|
||||
"title": "follower",
|
||||
"title": "seguidora",
|
||||
"footer": "Non se mostran seguidoras desde outros servidores."
|
||||
},
|
||||
"following": {
|
||||
"title": "following",
|
||||
"title": "seguindo",
|
||||
"footer": "Non se mostran os seguimentos desde outros servidores."
|
||||
},
|
||||
"familiarFollowers": {
|
||||
"title": "Followers you familiar",
|
||||
"followed_by_names": "Followed by %s"
|
||||
"title": "Seguimentos próximos",
|
||||
"followed_by_names": "Seguimentos de %s"
|
||||
},
|
||||
"favorited_by": {
|
||||
"title": "Favorited By"
|
||||
"title": "Favorecido por"
|
||||
},
|
||||
"reblogged_by": {
|
||||
"title": "Reblogged By"
|
||||
"title": "Promovido por"
|
||||
},
|
||||
"search": {
|
||||
"title": "Procurar",
|
||||
|
|
|
@ -124,8 +124,8 @@
|
|||
}
|
||||
},
|
||||
"status": {
|
||||
"user_reblogged": "%s hanno condiviso",
|
||||
"user_replied_to": "Rispondi a %s",
|
||||
"user_reblogged": "%s ha condiviso",
|
||||
"user_replied_to": "Risposta a %s",
|
||||
"show_post": "Mostra il post",
|
||||
"show_user_profile": "Mostra il profilo dell'utente",
|
||||
"content_warning": "Avviso sul contenuto",
|
||||
|
@ -343,7 +343,7 @@
|
|||
"title": "Inizio",
|
||||
"navigation_bar_state": {
|
||||
"offline": "Non in linea",
|
||||
"new_posts": "Vedi nuovi post",
|
||||
"new_posts": "Vedi i nuovi post",
|
||||
"published": "Pubblicato!",
|
||||
"Publishing": "Pubblicazione post...",
|
||||
"accessibility": {
|
||||
|
@ -533,7 +533,7 @@
|
|||
"notification_description": {
|
||||
"followed_you": "ti ha seguito",
|
||||
"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",
|
||||
"request_to_follow_you": "richiesta di seguirti",
|
||||
"poll_has_ended": "sondaggio terminato"
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"placeholder": "サーバーを探す",
|
||||
"search_servers_or_enter_url": "Search servers or enter URL"
|
||||
"search_servers_or_enter_url": "サーバーを検索またはURLを入力"
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "利用可能なサーバーの検索...",
|
||||
|
@ -617,46 +617,46 @@
|
|||
"text_placeholder": "追加コメントを入力",
|
||||
"reported": "報告済み",
|
||||
"step_one": {
|
||||
"step_1_of_4": "Step 1 of 4",
|
||||
"whats_wrong_with_this_post": "What's wrong with this post?",
|
||||
"whats_wrong_with_this_account": "What's wrong with this account?",
|
||||
"whats_wrong_with_this_username": "What's wrong with %s?",
|
||||
"select_the_best_match": "Select the best match",
|
||||
"i_dont_like_it": "I don’t like it",
|
||||
"it_is_not_something_you_want_to_see": "It is not something you want to see",
|
||||
"its_spam": "It’s spam",
|
||||
"malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies",
|
||||
"it_violates_server_rules": "It violates server rules",
|
||||
"you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules",
|
||||
"its_something_else": "It’s something else",
|
||||
"the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories"
|
||||
"step_1_of_4": "ステップ 1/4",
|
||||
"whats_wrong_with_this_post": "この投稿のどこが問題ですか?",
|
||||
"whats_wrong_with_this_account": "このアカウントのどこが問題ですか?",
|
||||
"whats_wrong_with_this_username": "%sさんのどこが問題ですか?",
|
||||
"select_the_best_match": "最も近いものを選んでください",
|
||||
"i_dont_like_it": "興味がありません",
|
||||
"it_is_not_something_you_want_to_see": "見たくない内容の場合",
|
||||
"its_spam": "これはスパムです",
|
||||
"malicious_links_fake_engagement_or_repetetive_replies": "悪意あるリンクや虚偽の情報、執拗な返信など",
|
||||
"it_violates_server_rules": "サーバーのルールに違反しています",
|
||||
"you_are_aware_that_it_breaks_specific_rules": "ルールに違反しているのを見つけた場合",
|
||||
"its_something_else": "その他",
|
||||
"the_issue_does_not_fit_into_other_categories": "当てはまる選択肢がない場合"
|
||||
},
|
||||
"step_two": {
|
||||
"step_2_of_4": "Step 2 of 4",
|
||||
"which_rules_are_being_violated": "Which rules are being violated?",
|
||||
"step_2_of_4": "ステップ 2/4",
|
||||
"which_rules_are_being_violated": "どのルールに違反していますか?",
|
||||
"select_all_that_apply": "Select all that apply",
|
||||
"i_just_don’t_like_it": "I just don’t like it"
|
||||
},
|
||||
"step_three": {
|
||||
"step_3_of_4": "Step 3 of 4",
|
||||
"step_3_of_4": "ステップ 3/4",
|
||||
"are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?",
|
||||
"select_all_that_apply": "Select all that apply"
|
||||
},
|
||||
"step_four": {
|
||||
"step_4_of_4": "Step 4 of 4",
|
||||
"is_there_anything_else_we_should_know": "Is there anything else we should know?"
|
||||
"step_4_of_4": "ステップ 4/4",
|
||||
"is_there_anything_else_we_should_know": "その他に私たちに伝えておくべき事はありますか?"
|
||||
},
|
||||
"step_final": {
|
||||
"dont_want_to_see_this": "Don’t want to see this?",
|
||||
"when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.",
|
||||
"unfollow": "Unfollow",
|
||||
"unfollowed": "Unfollowed",
|
||||
"unfollow_user": "Unfollow %s",
|
||||
"mute_user": "Mute %s",
|
||||
"unfollow": "フォロー解除",
|
||||
"unfollowed": "フォロー解除しました",
|
||||
"unfollow_user": "%sをフォロー解除",
|
||||
"mute_user": "%sをミュート",
|
||||
"you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.",
|
||||
"block_user": "Block %s",
|
||||
"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 they’ve 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": {
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Tifin n yiqeddacen yellan...",
|
||||
|
@ -251,7 +251,7 @@
|
|||
},
|
||||
"register": {
|
||||
"title": "Aha ad nebdu asbadu ɣef %s",
|
||||
"lets_get_you_set_up_on_domain": "Let’s get you set up on %s",
|
||||
"lets_get_you_set_up_on_domain": "Aha ad nebdu asbadu ɣef %s",
|
||||
"input": {
|
||||
"avatar": {
|
||||
"delete": "Kkes"
|
||||
|
@ -322,7 +322,7 @@
|
|||
"confirm_email": {
|
||||
"title": "Taɣawsa taneggarut.",
|
||||
"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": {
|
||||
"open_email_app": "Ldi asnas n yimayl",
|
||||
"resend": "Ales tuzna"
|
||||
|
@ -347,7 +347,7 @@
|
|||
"published": "Yettwasuffeɣ!",
|
||||
"Publishing": "Asuffeɣ tasuffeɣt...",
|
||||
"accessibility": {
|
||||
"logo_label": "Logo Button",
|
||||
"logo_label": "Taqeffalt n ulugu",
|
||||
"logo_hint": "Tap to scroll to top and tap again to previous location"
|
||||
}
|
||||
}
|
||||
|
@ -462,11 +462,11 @@
|
|||
}
|
||||
},
|
||||
"follower": {
|
||||
"title": "follower",
|
||||
"title": "aneḍfar",
|
||||
"footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."
|
||||
},
|
||||
"following": {
|
||||
"title": "following",
|
||||
"title": "yeṭṭafar",
|
||||
"footer": "Ineḍfaren seg yiqeddacen-nniḍen ur d-ttwaskanen ara."
|
||||
},
|
||||
"familiarFollowers": {
|
||||
|
@ -517,7 +517,7 @@
|
|||
"posts": "Tisuffaɣ",
|
||||
"hashtags": "Ihacṭagen",
|
||||
"news": "Isallen",
|
||||
"community": "Community",
|
||||
"community": "Tamɣiwent",
|
||||
"for_you": "I kečč·kem"
|
||||
},
|
||||
"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",
|
||||
"reported": "YETTWAMMEL",
|
||||
"step_one": {
|
||||
"step_1_of_4": "Step 1 of 4",
|
||||
"whats_wrong_with_this_post": "What's wrong with this post?",
|
||||
"step_1_of_4": "Aḥric 1 seg 4",
|
||||
"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_username": "What's wrong with %s?",
|
||||
"select_the_best_match": "Select the best match",
|
||||
"i_dont_like_it": "I don’t like it",
|
||||
"it_is_not_something_you_want_to_see": "It is not something you want to see",
|
||||
"its_spam": "It’s spam",
|
||||
"malicious_links_fake_engagement_or_repetetive_replies": "Malicious links, fake engagement, or repetetive replies",
|
||||
"it_violates_server_rules": "It violates server rules",
|
||||
"you_are_aware_that_it_breaks_specific_rules": "You are aware that it breaks specific rules",
|
||||
"its_something_else": "It’s something else",
|
||||
"the_issue_does_not_fit_into_other_categories": "The issue does not fit into other categories"
|
||||
"whats_wrong_with_this_username": "Acu n wugur yellan d %s?",
|
||||
"select_the_best_match": "Fren amṣada akk igerrzen",
|
||||
"i_dont_like_it": "Ur ḥemmleɣ ara aya",
|
||||
"it_is_not_something_you_want_to_see": "D ayen akk ur bɣiɣ ara ad waliɣ",
|
||||
"its_spam": "D aspam",
|
||||
"malicious_links_fake_engagement_or_repetetive_replies": "Yir iseɣwan, yir agman d tririyin i d-yettuɣalen",
|
||||
"it_violates_server_rules": "Truẓi n yilugan n uqeddac",
|
||||
"you_are_aware_that_it_breaks_specific_rules": "Teẓriḍ y•tettruẓu kra n yilugan",
|
||||
"its_something_else": "Ɣef ssebba-nniḍen",
|
||||
"the_issue_does_not_fit_into_other_categories": "Ugur ur yemṣada ara akk d taggayin-nniḍen"
|
||||
},
|
||||
"step_two": {
|
||||
"step_2_of_4": "Step 2 of 4",
|
||||
"which_rules_are_being_violated": "Which rules are being violated?",
|
||||
"select_all_that_apply": "Select all that apply",
|
||||
"i_just_don’t_like_it": "I just don’t like it"
|
||||
"step_2_of_4": "Aḥric 2 seg 4",
|
||||
"which_rules_are_being_violated": "Acu n yilugan i yettwarẓan?",
|
||||
"select_all_that_apply": "Fren akk tifrat ara yettusnasen",
|
||||
"i_just_don’t_like_it": "Ur ḥemmleɣ ara kan aya"
|
||||
},
|
||||
"step_three": {
|
||||
"step_3_of_4": "Step 3 of 4",
|
||||
"are_there_any_posts_that_back_up_this_report": "Are there any posts that back up this report?",
|
||||
"select_all_that_apply": "Select all that apply"
|
||||
"step_3_of_4": "Aḥric 3 seg 4",
|
||||
"are_there_any_posts_that_back_up_this_report": "Llant tsuffaɣ ara isdemren aneqqis-a?",
|
||||
"select_all_that_apply": "Fren akk tifrat ara yettusnasen"
|
||||
},
|
||||
"step_four": {
|
||||
"step_4_of_4": "Step 4 of 4",
|
||||
"is_there_anything_else_we_should_know": "Is there anything else we should know?"
|
||||
"step_4_of_4": "Aḥric 4 seg 4",
|
||||
"is_there_anything_else_we_should_know": "Yella wayen-nniḍen i ilaqen ad t-nẓer?"
|
||||
},
|
||||
"step_final": {
|
||||
"dont_want_to_see_this": "Don’t want to see this?",
|
||||
"when_you_see_something_you_dont_like_on_mastodon_you_can_remove_the_person_from_your_experience.": "When you see something you don’t like on Mastodon, you can remove the person from your experience.",
|
||||
"unfollow": "Unfollow",
|
||||
"unfollowed": "Unfollowed",
|
||||
"unfollow_user": "Unfollow %s",
|
||||
"mute_user": "Mute %s",
|
||||
"you_wont_see_their_posts_or_reblogs_in_your_home_feed_they_wont_know_they_ve_been_muted": "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.",
|
||||
"block_user": "Block %s",
|
||||
"they_will_no_longer_be_able_to_follow_or_see_your_posts_but_they_can_see_if_theyve_been_blocked": "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.",
|
||||
"while_we_review_this_you_can_take_action_against_user": "While we review this, you can take action against %s"
|
||||
"dont_want_to_see_this": "Ur tebɣiḍ ara ad twaliḍ aya?",
|
||||
"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": "Ur ṭṭafaṛ ara",
|
||||
"unfollowed": "Y•Teḥbes aḍfar n",
|
||||
"unfollow_user": "Y•Teḥbes aḍfar n %s",
|
||||
"mute_user": "Sgugem %s",
|
||||
"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": "Sewḥel %s",
|
||||
"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": "Ideg nekkni nessenqad tuttra-inek•inem, tzemreḍ ad tḥadreḍ mgal %s"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Peydakirina rajekarên berdest...",
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"placeholder": "ค้นหาเซิร์ฟเวอร์",
|
||||
"search_servers_or_enter_url": "Search servers or enter URL"
|
||||
"search_servers_or_enter_url": "ค้นหาเซิร์ฟเวอร์หรือป้อน URL"
|
||||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...",
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Đang tìm máy chủ hoạt động...",
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
},
|
||||
"input": {
|
||||
"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": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
|
|
|
@ -145,6 +145,8 @@
|
|||
DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */; };
|
||||
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72D26FDB24F00347686 /* SidebarListContentView.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 */; };
|
||||
DB0FCB6C27950E29006C02E2 /* MastodonMentionContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.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 */; };
|
||||
DB3EA8FC281BBAE100598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA8FB281BBAE100598866 /* AlamofireImage */; };
|
||||
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 */; };
|
||||
DB3EA904281BBD9400598866 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA903281BBD9400598866 /* Introspect */; };
|
||||
DB3EA906281BBE8200598866 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3EA905281BBE8200598866 /* AlamofireImage */; };
|
||||
|
@ -267,6 +268,7 @@
|
|||
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.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 */; };
|
||||
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; };
|
||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
|
||||
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.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 */; };
|
||||
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 */; };
|
||||
DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */; };
|
||||
DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; };
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; };
|
||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */; };
|
||||
|
@ -507,7 +510,6 @@
|
|||
DBB45B6027B50A4F002DC5A7 /* RecommendAccountItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */; };
|
||||
DBB45B6227B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */; };
|
||||
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 */; };
|
||||
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1136,6 +1140,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1285,7 +1290,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1429,6 +1433,7 @@
|
|||
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */,
|
||||
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
|
||||
|
@ -1469,7 +1474,6 @@
|
|||
EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */,
|
||||
DB3EA904281BBD9400598866 /* Introspect in Frameworks */,
|
||||
DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */,
|
||||
DB3EA900281BBB1D00598866 /* MetaTextKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1748,6 +1752,7 @@
|
|||
DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */,
|
||||
DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */,
|
||||
DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */,
|
||||
DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3004,8 +3009,8 @@
|
|||
DB9D6C0825E4F5A60051B173 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBB525132611EBB1002F1F29 /* Segmented */,
|
||||
DBB525462611ED57002F1F29 /* Header */,
|
||||
DBB525262611EBDA002F1F29 /* Paging */,
|
||||
DBB5253B2611ECF5002F1F29 /* Timeline */,
|
||||
DBE3CDF1261C6B3100430CC6 /* Favorite */,
|
||||
DB6B74F0272FB55400C70B6E /* Follower */,
|
||||
|
@ -3104,15 +3109,6 @@
|
|||
path = Video;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBB525132611EBB1002F1F29 /* Segmented */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBB525262611EBDA002F1F29 /* Paging */,
|
||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */,
|
||||
);
|
||||
path = Segmented;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBB525262611EBDA002F1F29 /* Paging */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3148,6 +3144,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
|
||||
DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */,
|
||||
DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */,
|
||||
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */,
|
||||
);
|
||||
path = View;
|
||||
|
@ -3448,6 +3446,7 @@
|
|||
DBA5A52E26F07ED800CACBAA /* PanModal */,
|
||||
DB3EA911281BBEA800598866 /* AlamofireImage */,
|
||||
DB3EA913281BBEA800598866 /* Alamofire */,
|
||||
DB486C0E282E41F200F69423 /* TabBarPager */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -3512,7 +3511,6 @@
|
|||
DB3EA8F4281BB65200598866 /* MastodonSDK */,
|
||||
DB3EA8FB281BBAE100598866 /* AlamofireImage */,
|
||||
DB3EA8FD281BBAF200598866 /* Alamofire */,
|
||||
DB3EA8FF281BBB1D00598866 /* MetaTextKit */,
|
||||
DB3EA901281BBD5D00598866 /* CommonOSLog */,
|
||||
DB3EA903281BBD9400598866 /* Introspect */,
|
||||
);
|
||||
|
@ -3665,11 +3663,11 @@
|
|||
DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */,
|
||||
DBF7A0FA26830C33004176A2 /* XCRemoteSwiftPackageReference "FPSIndicator" */,
|
||||
DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */,
|
||||
DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */,
|
||||
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
|
||||
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
|
||||
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -4037,7 +4035,6 @@
|
|||
DB0FCB7C2795821F006C02E2 /* StatusThreadRootTableViewCell.swift in Sources */,
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
DB5B54A32833BD1A00DEF8B2 /* UserListViewModel.swift in Sources */,
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||
DBF156DF2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift in Sources */,
|
||||
DB0617F1278413D00030EE79 /* PickServerServerSectionTableHeaderView.swift in Sources */,
|
||||
DB0FCB7E27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift in Sources */,
|
||||
|
@ -4256,6 +4253,7 @@
|
|||
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */,
|
||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
||||
DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */,
|
||||
DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */,
|
||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
|
||||
|
@ -4394,6 +4392,7 @@
|
|||
DB67D08627312E67006A36CF /* WizardViewController.swift in Sources */,
|
||||
DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */,
|
||||
DB9A489026035963008B817C /* APIService+Media.swift in Sources */,
|
||||
DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */,
|
||||
DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */,
|
||||
2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */,
|
||||
DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */,
|
||||
|
@ -4402,6 +4401,7 @@
|
|||
2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */,
|
||||
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
|
||||
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
|
||||
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
|
||||
DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */,
|
||||
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */,
|
||||
DB6D9F6F2635807F008423CD /* Setting.swift in Sources */,
|
||||
|
@ -4839,7 +4839,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4869,7 +4869,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -4977,11 +4977,11 @@
|
|||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 133;
|
||||
DYLIB_CURRENT_VERSION = 138;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = AppShared/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
|
@ -5008,11 +5008,11 @@
|
|||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 133;
|
||||
DYLIB_CURRENT_VERSION = 138;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = AppShared/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
|
@ -5103,7 +5103,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -5171,11 +5171,11 @@
|
|||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 133;
|
||||
DYLIB_CURRENT_VERSION = 138;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = AppShared/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
|
@ -5200,7 +5200,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5223,7 +5223,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5247,7 +5247,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = MastodonIntent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5271,7 +5271,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = MastodonIntent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5295,7 +5295,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = MastodonIntent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5319,7 +5319,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5343,7 +5343,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5430,7 +5430,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets";
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = Mastodon/Info.plist;
|
||||
|
@ -5497,11 +5497,11 @@
|
|||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 133;
|
||||
DYLIB_CURRENT_VERSION = 138;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = AppShared/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
|
@ -5525,7 +5525,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5548,7 +5548,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = ShareActionExtension/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5572,7 +5572,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = MastodonIntent/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5596,7 +5596,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5619,7 +5619,7 @@
|
|||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 133;
|
||||
CURRENT_PROJECT_VERSION = 138;
|
||||
DEVELOPMENT_TEAM = 5Z4GVSS33P;
|
||||
INFOPLIST_FILE = NotificationService/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
|
@ -5763,14 +5763,6 @@
|
|||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git";
|
||||
|
@ -5795,6 +5787,14 @@
|
|||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||
|
@ -5904,11 +5904,6 @@
|
|||
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||
productName = Alamofire;
|
||||
};
|
||||
DB3EA8FF281BBB1D00598866 /* MetaTextKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB01E23126A98F0900C3965B /* XCRemoteSwiftPackageReference "MetaTextKit" */;
|
||||
productName = MetaTextKit;
|
||||
};
|
||||
DB3EA901281BBD5D00598866 /* CommonOSLog */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||
|
@ -5959,6 +5954,11 @@
|
|||
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
|
||||
productName = Alamofire;
|
||||
};
|
||||
DB486C0E282E41F200F69423 /* TabBarPager */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */;
|
||||
productName = TabBarPager;
|
||||
};
|
||||
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<key>isShown</key>
|
||||
<true/>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>9</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -24,22 +24,22 @@
|
|||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>8</integer>
|
||||
<integer>11</integer>
|
||||
</dict>
|
||||
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>Mastodon - Snapshot.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>6</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ar.xcscheme</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>4</integer>
|
||||
<integer>8</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ar.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -114,7 +114,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>28</integer>
|
||||
<integer>30</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -134,7 +134,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>27</integer>
|
||||
<integer>31</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -105,8 +105,8 @@
|
|||
"repositoryURL": "https://github.com/TwidereProject/MetaTextKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2b9556a78b2986b8c0b04adc6da8ec206b448a0c",
|
||||
"version": "2.2.3"
|
||||
"revision": "dcd5255d6930c2fab408dc8562c577547e477624",
|
||||
"version": "2.2.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -208,6 +208,15 @@
|
|||
"version": "5.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "TabBarPager",
|
||||
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Tabman",
|
||||
"repositoryURL": "https://github.com/uias/Tabman",
|
||||
|
|
|
@ -342,6 +342,7 @@ extension SceneCoordinator {
|
|||
case .custom(let transitioningDelegate):
|
||||
viewController.modalPresentationStyle = .custom
|
||||
viewController.transitioningDelegate = transitioningDelegate
|
||||
// viewController.modalPresentationCapturesStatusBarAppearance = true
|
||||
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
|
||||
|
||||
case .customPush(let animated):
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,12 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
|
|||
private func previewImage() async {
|
||||
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 indexPathForSelectedRow = tableView.indexPathForSelectedRow,
|
||||
let cell = tableView.cellForRow(at: indexPathForSelectedRow) as? StatusViewContainerTableViewCell
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import UIKit
|
||||
|
||||
protocol ScrollViewContainer: UIViewController {
|
||||
var scrollView: UIScrollView? { get }
|
||||
var scrollView: UIScrollView { get }
|
||||
func scrollToTop(animated: Bool)
|
||||
}
|
||||
|
||||
extension ScrollViewContainer {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ final class AccountListTableViewCell: UITableViewCell {
|
|||
let checkmarkImageView: UIImageView = {
|
||||
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold))
|
||||
let imageView = UIImageView(image: image)
|
||||
imageView.tintColor = .label
|
||||
return imageView
|
||||
}()
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
|
|
@ -26,10 +26,14 @@ final class BadgeButton: UIButton {
|
|||
extension BadgeButton {
|
||||
private func _init() {
|
||||
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)
|
||||
setAppearance()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
setAppearance()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
@ -39,6 +43,12 @@ extension BadgeButton {
|
|||
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) {
|
||||
let number = min(99, max(0, number))
|
||||
setTitle("\(number)", for: .normal)
|
||||
|
|
|
@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryCommunityViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryCommunityViewController {
|
||||
|
|
|
@ -130,8 +130,8 @@ extension DiscoveryViewController {
|
|||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension DiscoveryViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
return (currentViewController as? ScrollViewContainer)?.scrollView
|
||||
var scrollView: UIScrollView {
|
||||
return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryForYouViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryHashtagsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryHashtagsViewController {
|
||||
|
|
|
@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryNewsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension DiscoveryNewsViewController {
|
||||
|
|
|
@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
|
|||
|
||||
// MARK: ScrollViewContainer
|
||||
extension DiscoveryPostsViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
tableView
|
||||
}
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
// MARK: - DiscoveryIntroBannerViewDelegate
|
||||
|
|
|
@ -537,13 +537,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
|
|||
// MARK: - ScrollViewContainer
|
||||
extension HomeTimelineViewController: ScrollViewContainer {
|
||||
|
||||
var scrollView: UIScrollView? { return tableView }
|
||||
var scrollView: UIScrollView { return tableView }
|
||||
|
||||
func scrollToTop(animated: Bool) {
|
||||
guard let scrollView = scrollView else {
|
||||
return
|
||||
}
|
||||
|
||||
if scrollView.contentOffset.y < scrollView.frame.height,
|
||||
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
||||
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,
|
||||
|
|
|
@ -135,6 +135,18 @@ extension MediaPreviewViewController {
|
|||
}
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
|
|||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension NotificationTimelineViewController: ScrollViewContainer {
|
||||
|
||||
var scrollView: UIScrollView? { tableView }
|
||||
|
||||
var scrollView: UIScrollView { tableView }
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewController {
|
||||
|
|
|
@ -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)")
|
||||
await self.enter(state: Fail.self)
|
||||
}
|
||||
} // Task
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,6 @@ extension NotificationTimelineViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
var excludeTypes: [MastodonNotificationType]? {
|
||||
switch self {
|
||||
case .everything: return nil
|
||||
|
|
|
@ -170,9 +170,9 @@ extension NotificationViewController {
|
|||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension NotificationViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
var scrollView: UIScrollView {
|
||||
guard let viewController = currentViewController as? NotificationTimelineViewController else {
|
||||
return nil
|
||||
return UIScrollView()
|
||||
}
|
||||
return viewController.scrollView
|
||||
}
|
||||
|
|
|
@ -9,6 +9,9 @@ import os.log
|
|||
import UIKit
|
||||
import Combine
|
||||
import MetaTextKit
|
||||
import MastodonLocalization
|
||||
import TabBarPager
|
||||
import XLPagerTabStrip
|
||||
|
||||
protocol ProfileAboutViewControllerDelegate: AnyObject {
|
||||
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
|
||||
|
@ -162,7 +165,17 @@ extension ProfileAboutViewController: ProfileFieldEditCollectionViewCellDelegate
|
|||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension ProfileAboutViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? {
|
||||
collectionView
|
||||
var scrollView: UIScrollView { 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ extension ProfileAboutViewModel {
|
|||
profileFieldEditCollectionViewCellDelegate: profileFieldEditCollectionViewCellDelegate
|
||||
)
|
||||
)
|
||||
self.diffableDataSource = diffableDataSource
|
||||
|
||||
diffableDataSource.reorderingHandlers.canReorderItem = { item -> Bool in
|
||||
switch item {
|
||||
|
@ -42,15 +43,18 @@ extension ProfileAboutViewModel {
|
|||
guard case let .editField(field) = item else { continue }
|
||||
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(
|
||||
$isEditing.removeDuplicates(),
|
||||
displayProfileInfo.$fields.removeDuplicates(),
|
||||
editProfileInfo.$fields.removeDuplicates(),
|
||||
profileInfo.$fields.removeDuplicates(),
|
||||
profileInfoEditing.$fields.removeDuplicates(),
|
||||
$emojiMeta.removeDuplicates()
|
||||
)
|
||||
.throttle(for: 0.3, scheduler: DispatchQueue.main, latest: true)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import Kanna
|
||||
|
@ -18,41 +19,69 @@ final class ProfileAboutViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var user: MastodonUser?
|
||||
@Published var isEditing = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
@Published var emojiMeta: MastodonContent.Emojis = [:]
|
||||
|
||||
// output
|
||||
var diffableDataSource: UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>?
|
||||
let profileInfo = ProfileInfo()
|
||||
let profileInfoEditing = ProfileInfo()
|
||||
|
||||
let displayProfileInfo = ProfileInfo()
|
||||
let editProfileInfo = ProfileInfo()
|
||||
let editProfileInfoDidInitialized = CurrentValueSubject<Void, Never>(Void()) // needs trigger initial event
|
||||
@Published var fields: [MastodonField] = []
|
||||
@Published var emojiMeta: MastodonContent.Emojis = [:]
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
// 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(
|
||||
$isEditing.removeDuplicates(), // only trigger when value toggle
|
||||
$accountForEdit
|
||||
$fields,
|
||||
$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)
|
||||
.sink { [weak self] isEditing, account in
|
||||
.sink { [weak self] account, emojiMeta in
|
||||
guard let self = self else { return }
|
||||
guard isEditing else { return }
|
||||
guard let account = account else { return }
|
||||
|
||||
// setup editing value when toggle to editing
|
||||
self.editProfileInfo.fields = account?.source?.fields?.compactMap { field in
|
||||
self.profileInfo.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(
|
||||
name: field.name,
|
||||
value: field.value,
|
||||
emojiMeta: [:] // no use for editing
|
||||
)
|
||||
} ?? []
|
||||
self.editProfileInfoDidInitialized.send()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -65,31 +94,31 @@ extension ProfileAboutViewModel {
|
|||
|
||||
extension ProfileAboutViewModel {
|
||||
func appendFieldItem() {
|
||||
var fields = editProfileInfo.fields
|
||||
var fields = profileInfoEditing.fields
|
||||
guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return }
|
||||
fields.append(ProfileFieldItem.FieldValue(name: "", value: "", emojiMeta: [:]))
|
||||
editProfileInfo.fields = fields
|
||||
profileInfoEditing.fields = fields
|
||||
}
|
||||
|
||||
func removeFieldItem(item: ProfileFieldItem) {
|
||||
var fields = editProfileInfo.fields
|
||||
var fields = profileInfoEditing.fields
|
||||
guard case let .editField(field) = item else { return }
|
||||
guard let removeIndex = fields.firstIndex(of: field) else { return }
|
||||
fields.remove(at: removeIndex)
|
||||
editProfileInfo.fields = fields
|
||||
profileInfoEditing.fields = fields
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ProfileViewModelEditable
|
||||
extension ProfileAboutViewModel: ProfileViewModelEditable {
|
||||
func isEdited() -> Bool {
|
||||
var isEdited: Bool {
|
||||
guard isEditing else { return false }
|
||||
|
||||
let isFieldsEqual: Bool = {
|
||||
let originalFields = self.accountForEdit?.source?.fields?.compactMap { field in
|
||||
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 }
|
||||
for (editField, originalField) in zip(editFields, originalFields) {
|
||||
guard editField.name.value == originalField.name.value,
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import PhotosUI
|
||||
import AlamofireImage
|
||||
import CropViewController
|
||||
|
@ -15,22 +16,31 @@ import MastodonMeta
|
|||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
import Tabman
|
||||
import TabBarPager
|
||||
|
||||
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 headerMinHeight: CGFloat = segmentedControlHeight
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: ProfileHeaderViewModel!
|
||||
|
||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||
weak var headerDelegate: TabBarPagerHeaderDelegate?
|
||||
|
||||
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||
|
||||
let titleView: DoubleTitleLabelNavigationBarTitleView = {
|
||||
let titleView = DoubleTitleLabelNavigationBarTitleView()
|
||||
titleView.titleLabel.textColor = .white
|
||||
|
@ -44,38 +54,7 @@ final class ProfileHeaderViewController: UIViewController {
|
|||
|
||||
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() {
|
||||
// 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 isBannerPinned = false
|
||||
|
||||
// private var isAdjustBannerImageViewForSafeAreaInset = false
|
||||
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
||||
|
@ -103,7 +82,7 @@ final class ProfileHeaderViewController: UIViewController {
|
|||
}()
|
||||
|
||||
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() {
|
||||
super.viewDidLoad()
|
||||
|
||||
customizeButtonBarAppearance()
|
||||
view.setContentHuggingPriority(.required - 1, for: .vertical)
|
||||
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
|
||||
ThemeService.shared.currentTheme
|
||||
|
@ -124,117 +103,15 @@ extension ProfileHeaderViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// profileHeaderView.preservesSuperviewLayoutMargins = true
|
||||
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(profileHeaderView)
|
||||
NSLayoutConstraint.activate([
|
||||
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
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
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: profileHeaderView.nameTextField)
|
||||
|
@ -242,18 +119,56 @@ extension ProfileHeaderViewController {
|
|||
.sink { [weak self] notification in
|
||||
guard let self = self 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)
|
||||
|
||||
profileHeaderView.editAvatarButton.menu = createAvatarContextMenu()
|
||||
profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true
|
||||
profileHeaderView.editAvatarButtonOverlayIndicatorView.menu = createAvatarContextMenu()
|
||||
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) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppear.value = true
|
||||
profileHeaderView.viewModel.viewDidAppear.send()
|
||||
|
||||
// set display after view appear
|
||||
profileHeaderView.setupAvatarOverlayViews()
|
||||
|
@ -262,14 +177,7 @@ extension ProfileHeaderViewController {
|
|||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view)
|
||||
setupBottomShadow()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
customizeButtonBarAppearance()
|
||||
headerDelegate?.viewLayoutDidUpdate(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -321,56 +229,8 @@ extension ProfileHeaderViewController {
|
|||
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) {
|
||||
// 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
|
||||
}
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||
|
||||
// set title view offset
|
||||
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 transformY = max(0, titleViewContentOffset)
|
||||
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 {
|
||||
viewModel.isTitleViewContentOffsetSet.value = true
|
||||
}
|
||||
|
||||
// set avatar fade
|
||||
if progress > 0 {
|
||||
setProfileAvatar(alpha: 0)
|
||||
} else if progress > -abs(throttle) {
|
||||
// y = -(1/0.8T)x
|
||||
let alpha = -1 / abs(0.8 * throttle) * progress
|
||||
if progress > 0, throttle > 0 {
|
||||
// y = 1 - (x/t)
|
||||
// give: x = 0, y = 1
|
||||
// x = t, y = 0
|
||||
let alpha = 1 - progress/throttle
|
||||
setProfileAvatar(alpha: alpha)
|
||||
} else {
|
||||
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
|
||||
extension ProfileHeaderViewController: MetaTextDelegate {
|
||||
func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
|
||||
|
@ -412,7 +365,9 @@ extension ProfileHeaderViewController: MetaTextDelegate {
|
|||
switch metaText {
|
||||
case profileHeaderView.bioMetaText:
|
||||
guard viewModel.isEditing else { break }
|
||||
viewModel.editProfileInfo.note = metaText.backedString
|
||||
defer {
|
||||
viewModel.profileInfoEditing.note = metaText.backedString
|
||||
}
|
||||
let metaContent = PlaintextMetaContent(string: metaText.backedString)
|
||||
return metaContent
|
||||
default:
|
||||
|
@ -484,7 +439,10 @@ extension ProfileHeaderViewController: UIDocumentPickerDelegate {
|
|||
// MARK: - CropViewControllerDelegate
|
||||
extension ProfileHeaderViewController: CropViewControllerDelegate {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TabBarPagerHeader
|
||||
extension ProfileHeaderViewController: TabBarPagerHeader { }
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import Kanna
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonUI
|
||||
|
||||
final class ProfileHeaderViewModel {
|
||||
|
||||
|
@ -21,37 +23,42 @@ final class ProfileHeaderViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var isEditing = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
@Published var emojiMeta: MastodonContent.Emojis = [:]
|
||||
@Published var user: MastodonUser?
|
||||
@Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
|
||||
|
||||
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||
let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
|
||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||
@Published var isEditing = false
|
||||
@Published var isUpdating = false
|
||||
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
|
||||
// let needsFiledCollectionViewHidden = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
// output
|
||||
let isTitleViewDisplaying = CurrentValueSubject<Bool, Never>(false)
|
||||
let displayProfileInfo = ProfileInfo()
|
||||
let editProfileInfo = ProfileInfo()
|
||||
let editProfileInfoDidInitialized = CurrentValueSubject<Void, Never>(Void()) // needs trigger initial event
|
||||
let profileInfo = ProfileInfo()
|
||||
let profileInfoEditing = ProfileInfo()
|
||||
|
||||
@Published var isTitleViewDisplaying = false
|
||||
@Published var isTitleViewContentOffsetSet = false
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
|
||||
Publishers.CombineLatest(
|
||||
$isEditing.removeDuplicates(), // only trigger when value toggle
|
||||
$accountForEdit
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEditing, account in
|
||||
.sink { [weak self] account in
|
||||
guard let self = self else { return }
|
||||
guard isEditing else { return }
|
||||
// setup editing value when toggle to editing
|
||||
self.editProfileInfo.name = self.displayProfileInfo.name // set to name
|
||||
self.editProfileInfo.avatarImage = nil // set to empty
|
||||
self.editProfileInfo.note = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note)
|
||||
self.editProfileInfoDidInitialized.send()
|
||||
guard let account = account else { return }
|
||||
// avatar
|
||||
self.profileInfo.avatar = nil
|
||||
self.profileInfoEditing.avatar = nil
|
||||
// name
|
||||
let name = account.displayNameWithFallback
|
||||
self.profileInfo.name = name
|
||||
self.profileInfoEditing.name = name
|
||||
// bio
|
||||
let note = ProfileHeaderViewModel.normalize(note: account.note)
|
||||
self.profileInfo.note = note
|
||||
self.profileInfoEditing.note = note
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
@ -61,29 +68,9 @@ final class ProfileHeaderViewModel {
|
|||
extension ProfileHeaderViewModel {
|
||||
class ProfileInfo {
|
||||
// input
|
||||
@Published var avatar: UIImage?
|
||||
@Published var name: String?
|
||||
@Published var avatarImageURL: URL?
|
||||
@Published var avatarImage: UIImage?
|
||||
@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
|
||||
extension ProfileHeaderViewModel: ProfileViewModelEditable {
|
||||
func isEdited() -> Bool {
|
||||
var isEdited: Bool {
|
||||
guard isEditing else { return false }
|
||||
|
||||
guard editProfileInfo.name == displayProfileInfo.name else { return true }
|
||||
guard editProfileInfo.avatarImage == nil else { return true }
|
||||
guard editProfileInfo.note == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note) else { return true }
|
||||
guard profileInfoEditing.avatar == nil else { return true }
|
||||
guard profileInfo.name == profileInfoEditing.name else { return true }
|
||||
guard profileInfo.note == profileInfoEditing.note else { return true }
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -38,7 +38,15 @@ final class ProfileHeaderView: UIView {
|
|||
weak var delegate: ProfileHeaderViewDelegate?
|
||||
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 bannerImageView: UIImageView = {
|
||||
|
@ -61,6 +69,8 @@ final class ProfileHeaderView: UIView {
|
|||
overlayView.backgroundColor = ProfileHeaderView.bannerImageViewOverlayViewBackgroundNormalColor
|
||||
return overlayView
|
||||
}()
|
||||
var bannerImageViewTopLayoutConstraint: NSLayoutConstraint!
|
||||
var bannerImageViewBottomLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let avatarImageViewBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
|
@ -81,7 +91,7 @@ final class ProfileHeaderView: UIView {
|
|||
|
||||
func setupAvatarOverlayViews() {
|
||||
editAvatarBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||
editAvatarButton.tintColor = .white
|
||||
editAvatarButtonOverlayIndicatorView.tintColor = .white
|
||||
}
|
||||
|
||||
static let avatarImageViewOverlayBlurEffect = UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||
|
@ -101,7 +111,7 @@ final class ProfileHeaderView: UIView {
|
|||
return view
|
||||
}()
|
||||
|
||||
let editAvatarButton: HighlightDimmableButton = {
|
||||
let editAvatarButtonOverlayIndicatorView: HighlightDimmableButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 28)), for: .normal)
|
||||
button.tintColor = .clear
|
||||
|
@ -136,7 +146,7 @@ final class ProfileHeaderView: UIView {
|
|||
let nameTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
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.autocorrectionType = .no
|
||||
textField.autocapitalizationType = .none
|
||||
|
@ -164,8 +174,8 @@ final class ProfileHeaderView: UIView {
|
|||
return button
|
||||
}()
|
||||
|
||||
let bioContainerView = UIView()
|
||||
let fieldContainerStackView = UIStackView()
|
||||
// let bioContainerView = UIView()
|
||||
// let fieldContainerStackView = UIStackView()
|
||||
|
||||
let bioMetaText: MetaText = {
|
||||
let metaText = MetaText()
|
||||
|
@ -230,12 +240,19 @@ extension ProfileHeaderView {
|
|||
bannerContainerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
bannerContainerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
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.frame = bannerContainerView.bounds
|
||||
bannerImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
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
|
||||
bannerImageView.addSubview(bannerImageViewOverlayVisualEffectView)
|
||||
|
@ -283,13 +300,13 @@ extension ProfileHeaderView {
|
|||
editAvatarBackgroundView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor),
|
||||
])
|
||||
|
||||
editAvatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
editAvatarBackgroundView.addSubview(editAvatarButton)
|
||||
editAvatarButtonOverlayIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
editAvatarBackgroundView.addSubview(editAvatarButtonOverlayIndicatorView)
|
||||
NSLayoutConstraint.activate([
|
||||
editAvatarButton.topAnchor.constraint(equalTo: editAvatarBackgroundView.topAnchor),
|
||||
editAvatarButton.leadingAnchor.constraint(equalTo: editAvatarBackgroundView.leadingAnchor),
|
||||
editAvatarButton.trailingAnchor.constraint(equalTo: editAvatarBackgroundView.trailingAnchor),
|
||||
editAvatarButton.bottomAnchor.constraint(equalTo: editAvatarBackgroundView.bottomAnchor),
|
||||
editAvatarButtonOverlayIndicatorView.topAnchor.constraint(equalTo: editAvatarBackgroundView.topAnchor),
|
||||
editAvatarButtonOverlayIndicatorView.leadingAnchor.constraint(equalTo: editAvatarBackgroundView.leadingAnchor),
|
||||
editAvatarButtonOverlayIndicatorView.trailingAnchor.constraint(equalTo: editAvatarBackgroundView.trailingAnchor),
|
||||
editAvatarButtonOverlayIndicatorView.bottomAnchor.constraint(equalTo: editAvatarBackgroundView.bottomAnchor),
|
||||
])
|
||||
editAvatarBackgroundView.isUserInteractionEnabled = true
|
||||
avatarButton.isUserInteractionEnabled = true
|
||||
|
@ -297,6 +314,7 @@ extension ProfileHeaderView {
|
|||
// container: V - [ dashboard container | author container | bio ]
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.distribution = .fill
|
||||
container.spacing = 8
|
||||
container.preservesSuperviewLayoutMargins = true
|
||||
container.isLayoutMarginsRelativeArrangement = true
|
||||
|
@ -364,6 +382,7 @@ extension ProfileHeaderView {
|
|||
nameTextFieldBackgroundView.trailingAnchor.constraint(equalTo: nameMetaText.textView.trailingAnchor, constant: 5),
|
||||
nameMetaText.textView.bottomAnchor.constraint(equalTo: nameTextFieldBackgroundView.bottomAnchor),
|
||||
])
|
||||
// nameMetaText.textView.setContentHuggingPriority(, for: <#T##NSLayoutConstraint.Axis#>)
|
||||
|
||||
nameContainerStackView.addArrangedSubview(displayNameStackView)
|
||||
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 {
|
||||
@objc private func relationshipActionButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
@ -27,88 +27,100 @@ class ProfileViewModel: NSObject {
|
|||
private var mastodonUserObserver: AnyCancellable?
|
||||
private var currentMastodonUserObserver: AnyCancellable?
|
||||
|
||||
let postsUserTimelineViewModel: UserTimelineViewModel
|
||||
let repliesUserTimelineViewModel: UserTimelineViewModel
|
||||
let mediaUserTimelineViewModel: UserTimelineViewModel
|
||||
let profileAboutViewModel: ProfileAboutViewModel
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var me: MastodonUser?
|
||||
@Published var user: MastodonUser?
|
||||
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
@Published var isEditing = false
|
||||
@Published var isUpdating = false
|
||||
@Published var accountForEdit: Mastodon.Entity.Account?
|
||||
|
||||
// output
|
||||
let domain: CurrentValueSubject<String?, Never>
|
||||
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>
|
||||
let relationshipViewModel = RelationshipViewModel()
|
||||
|
||||
// fulfill this before editing
|
||||
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
|
||||
@Published var userIdentifier: UserIdentifier? = nil
|
||||
|
||||
let protected: CurrentValueSubject<Bool?, Never>
|
||||
let suspended: CurrentValueSubject<Bool, Never>
|
||||
@Published var isRelationshipActionButtonHidden: Bool = true
|
||||
@Published var isReplyBarButtonItemHidden: Bool = true
|
||||
@Published var isMoreMenuBarButtonItemHidden: Bool = true
|
||||
@Published var isMeBarButtonItemsHidden: Bool = true
|
||||
@Published var isPagingEnabled = true
|
||||
|
||||
let isEditing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isUpdating = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
|
||||
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)
|
||||
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||
let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
|
||||
// @Published var protected: Bool? = nil
|
||||
// let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||
self.context = context
|
||||
self.user = mastodonUser
|
||||
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
|
||||
self.userID = CurrentValueSubject(mastodonUser?.id)
|
||||
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
|
||||
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
|
||||
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
|
||||
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
|
||||
self.bioDescription = CurrentValueSubject(mastodonUser?.note)
|
||||
self.url = CurrentValueSubject(mastodonUser?.url)
|
||||
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
|
||||
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
|
||||
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
|
||||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
||||
self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
|
||||
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:])
|
||||
self.postsUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
title: L10n.Scene.Profile.SegmentedControl.posts,
|
||||
queryFilter: .init(excludeReplies: true)
|
||||
)
|
||||
self.repliesUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
title: L10n.Scene.Profile.SegmentedControl.postsAndReplies,
|
||||
queryFilter: .init(excludeReplies: true)
|
||||
)
|
||||
self.mediaUserTimelineViewModel = UserTimelineViewModel(
|
||||
context: context,
|
||||
title: L10n.Scene.Profile.SegmentedControl.media,
|
||||
queryFilter: .init(onlyMedia: true)
|
||||
)
|
||||
self.profileAboutViewModel = ProfileAboutViewModel(context: context)
|
||||
super.init()
|
||||
|
||||
relationshipActionOptionSet
|
||||
.compactMap { $0.highPriorityAction(except: []) }
|
||||
.map { $0 == .none }
|
||||
.assign(to: \.value, on: isRelationshipActionButtonHidden)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind active authentication
|
||||
// bind me
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] authenticationBox in
|
||||
guard let self = self else { return }
|
||||
guard let authenticationBox = authenticationBox else {
|
||||
self.domain.value = nil
|
||||
self.me = nil
|
||||
self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
}
|
||||
.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
|
||||
}
|
||||
self.domain.value = authenticationBox.domain
|
||||
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
|
||||
|
||||
let isMyself = optionSet.contains(.isMyself)
|
||||
self.isReplyBarButtonItemHidden = isMyself
|
||||
self.isMoreMenuBarButtonItemHidden = isMyself
|
||||
self.isMeBarButtonItemsHidden = !isMyself
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
@ -148,198 +160,23 @@ class ProfileViewModel: NSObject {
|
|||
} 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)")
|
||||
}
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
//
|
||||
let isBlockingOrBlocked = Publishers.CombineLatest(
|
||||
isBlocking,
|
||||
isBlockedBy
|
||||
relationshipViewModel.$isBlocking,
|
||||
relationshipViewModel.$isBlockingBy
|
||||
)
|
||||
.map { $0 || $1 }
|
||||
.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
|
||||
isBlockingOrBlocked,
|
||||
$isEditing
|
||||
)
|
||||
.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 {
|
||||
self.relationshipActionOptionSet.value = [.edit]
|
||||
// set bar button item state
|
||||
self.isReplyBarButtonItemHidden.value = true
|
||||
self.isMoreMenuBarButtonItemHidden.value = true
|
||||
self.isMeBarButtonItemsHidden.value = false
|
||||
} 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
|
||||
}
|
||||
.map { !$0 && !$1 }
|
||||
.assign(to: &$isPagingEnabled)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -386,7 +223,7 @@ extension ProfileViewModel {
|
|||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
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 {
|
||||
return image.af.imageScaled(to: ProfileHeaderViewModel.avatarImageMaxSizeInPixel)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,8 @@ import AVKit
|
|||
import Combine
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import TabBarPager
|
||||
import XLPagerTabStrip
|
||||
|
||||
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
|
@ -143,7 +145,14 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView
|
|||
|
||||
// MARK: - CustomScrollViewContainerController
|
||||
extension UserTimelineViewController: ScrollViewContainer {
|
||||
var scrollView: UIScrollView? { return tableView }
|
||||
var scrollView: UIScrollView { return tableView }
|
||||
}
|
||||
|
||||
// MARK: - TabBarPage
|
||||
extension UserTimelineViewController: TabBarPage {
|
||||
var pageScrollView: UIScrollView {
|
||||
scrollView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
|
@ -165,3 +174,10 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable {
|
|||
statusKeyCommandHandler(sender)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IndicatorInfoProvider
|
||||
extension UserTimelineViewController: IndicatorInfoProvider {
|
||||
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
|
||||
return IndicatorInfo(title: viewModel.title)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,8 @@ extension UserTimelineViewModel {
|
|||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot)
|
||||
|
||||
// trigger user timeline loading
|
||||
Publishers.CombineLatest(
|
||||
$domain.removeDuplicates(),
|
||||
$userID.removeDuplicates()
|
||||
)
|
||||
// trigger timeline reloading
|
||||
$userIdentifier
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
|
@ -43,9 +40,9 @@ extension UserTimelineViewModel {
|
|||
.store(in: &disposeBag)
|
||||
|
||||
let needsTimelineHidden = Publishers.CombineLatest3(
|
||||
isBlocking,
|
||||
isBlockedBy,
|
||||
isSuspended
|
||||
$isBlocking,
|
||||
$isBlockedBy,
|
||||
$isSuspended
|
||||
).map { $0 || $1 || $2 }
|
||||
|
||||
Publishers.CombineLatest(
|
||||
|
|
|
@ -50,7 +50,7 @@ extension UserTimelineViewModel.State {
|
|||
guard let viewModel = viewModel else { return false }
|
||||
switch stateClass {
|
||||
case is Reloading.Type:
|
||||
return viewModel.userID != nil
|
||||
return viewModel.userIdentifier != nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ extension UserTimelineViewModel.State {
|
|||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ extension UserTimelineViewModel.State {
|
|||
guard let viewModel = viewModel, let _ = stateMachine else { return }
|
||||
|
||||
// trigger data source update. otherwise, spinner always display
|
||||
viewModel.isSuspended.value = viewModel.isSuspended.value
|
||||
viewModel.isSuspended = viewModel.isSuspended
|
||||
|
||||
// remove bottom loader
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
|
|
|
@ -19,17 +19,18 @@ final class UserTimelineViewModel {
|
|||
|
||||
// input
|
||||
let context: AppContext
|
||||
@Published var domain: String?
|
||||
@Published var userID: String?
|
||||
@Published var queryFilter: QueryFilter
|
||||
let title: String
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
@Published var userIdentifier: UserIdentifier?
|
||||
@Published var queryFilter: QueryFilter
|
||||
|
||||
let isBlocking = CurrentValueSubject<Bool, Never>(false)
|
||||
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
|
||||
let isSuspended = CurrentValueSubject<Bool, Never>(false)
|
||||
let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
|
||||
var dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
||||
@Published var isBlocking = false
|
||||
@Published var isBlockedBy = false
|
||||
@Published var isSuspended = false
|
||||
|
||||
// let userDisplayName = CurrentValueSubject<String?, Never>(nil) // for suspended prompt label
|
||||
// var dataSourceDidUpdate = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, StatusItem>?
|
||||
|
@ -48,30 +49,27 @@ final class UserTimelineViewModel {
|
|||
|
||||
init(
|
||||
context: AppContext,
|
||||
domain: String?,
|
||||
userID: String?,
|
||||
title: String,
|
||||
queryFilter: QueryFilter
|
||||
) {
|
||||
self.context = context
|
||||
self.title = title
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: domain,
|
||||
additionalTweetPredicate: Status.notDeleted()
|
||||
domain: nil,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
self.domain = domain
|
||||
self.userID = userID
|
||||
self.queryFilter = queryFilter
|
||||
// super.init()
|
||||
|
||||
$domain
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0?.domain }
|
||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
// Make status bar style adaptive for child view controller
|
||||
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||
class AdaptiveStatusBarStyleNavigationController: UINavigationController {
|
||||
|
||||
private lazy var fullWidthBackGestureRecognizer = UIPanGestureRecognizer()
|
||||
|
||||
// Make status bar style adaptive for child view controller
|
||||
// SeeAlso: `modalPresentationCapturesStatusBarAppearance`
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
visibleViewController
|
||||
}
|
||||
|
|
|
@ -101,7 +101,9 @@ extension PollOptionView {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] theme in
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,28 @@ extension APIService {
|
|||
|
||||
let query = Mastodon.API.Notifications.Query(
|
||||
maxID: maxID,
|
||||
types: {
|
||||
switch scope {
|
||||
case .everything:
|
||||
return [
|
||||
.follow,
|
||||
.followRequest,
|
||||
.mention,
|
||||
.reblog,
|
||||
.favourite,
|
||||
.poll,
|
||||
.status,
|
||||
]
|
||||
case .mentions:
|
||||
return [
|
||||
.follow,
|
||||
.followRequest,
|
||||
.reblog,
|
||||
.favourite,
|
||||
.poll
|
||||
]
|
||||
}
|
||||
}(),
|
||||
excludeTypes: {
|
||||
switch scope {
|
||||
case .everything:
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
|
|
@ -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/kean/Nuke.git", from: "10.3.1"),
|
||||
.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/AlamofireImage.git", from: "4.1.0"),
|
||||
.package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"),
|
||||
|
|
|
@ -305,12 +305,12 @@ téléversé sur Mastodon.";
|
|||
"Scene.Report.StepFinal.BlockUser" = "Bloquer %@";
|
||||
"Scene.Report.StepFinal.DontWantToSeeThis" = "Vous ne voulez pas voir cela ?";
|
||||
"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 they’ve been blocked.";
|
||||
"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "Ils ne seront plus en mesure de suivre ou de voir vos messages, mais iels peuvent voir s’iels ont été bloqué·e·s.";
|
||||
"Scene.Report.StepFinal.Unfollow" = "Se désabonner";
|
||||
"Scene.Report.StepFinal.UnfollowUser" = "Ne plus suivre %@";
|
||||
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed";
|
||||
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "Quand vous voyez quelque chose que vous n’aimez 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 qu’iels ont été mis en sourdine.";
|
||||
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Y a-t-il autre chose que nous devrions savoir ?";
|
||||
"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.ItsSpam" = "C’est du spam";
|
||||
"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.TheIssueDoesNotFitIntoOtherCategories" = "Le problème ne correspond à aucune des catégories";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "Qu’est-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.NoResults" = "Aucun résultat";
|
||||
"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.Language" = "LANGUE";
|
||||
"Scene.ServerPicker.Label.Users" = "UTILISATEUR·RICE·S";
|
||||
|
|
|
@ -208,14 +208,14 @@ ser subido a Mastodon.";
|
|||
"Scene.Discovery.Tabs.Hashtags" = "Cancelos";
|
||||
"Scene.Discovery.Tabs.News" = "Novas";
|
||||
"Scene.Discovery.Tabs.Posts" = "Publicacións";
|
||||
"Scene.Familiarfollowers.FollowedByNames" = "Followed by %@";
|
||||
"Scene.Familiarfollowers.Title" = "Followers you familiar";
|
||||
"Scene.Familiarfollowers.FollowedByNames" = "Seguimentos de %@";
|
||||
"Scene.Familiarfollowers.Title" = "Seguimentos próximos";
|
||||
"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.Title" = "follower";
|
||||
"Scene.Follower.Title" = "seguidora";
|
||||
"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.LogoLabel" = "Botón do logo";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Novas publicacións";
|
||||
|
@ -259,7 +259,7 @@ ser subido a Mastodon.";
|
|||
"Scene.Profile.SegmentedControl.Posts" = "Publicacións";
|
||||
"Scene.Profile.SegmentedControl.PostsAndReplies" = "Publicacións e 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.Email" = "Email";
|
||||
"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.NoResults" = "Sen resultados";
|
||||
"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.Language" = "IDIOMA";
|
||||
"Scene.ServerPicker.Label.Users" = "USUARIAS";
|
||||
|
|
|
@ -118,8 +118,8 @@ Per favore verifica la tua connessione internet.";
|
|||
"Common.Controls.Status.Tag.Mention" = "Menzione";
|
||||
"Common.Controls.Status.Tag.Url" = "URL";
|
||||
"Common.Controls.Status.TapToReveal" = "Tocca per rivelare";
|
||||
"Common.Controls.Status.UserReblogged" = "%@ hanno condiviso";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Rispondi a %@";
|
||||
"Common.Controls.Status.UserReblogged" = "%@ ha condiviso";
|
||||
"Common.Controls.Status.UserRepliedTo" = "Risposta a %@";
|
||||
"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.PrivateFromMe" = "Solo i miei seguaci possono vedere questo post.";
|
||||
|
@ -218,7 +218,7 @@ caricato su Mastodon.";
|
|||
"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.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.Published" = "Pubblicato!";
|
||||
"Scene.HomeTimeline.NavigationBarState.Publishing" = "Pubblicazione post...";
|
||||
|
@ -229,7 +229,7 @@ caricato su Mastodon.";
|
|||
"Scene.Notification.NotificationDescription.FollowedYou" = "ti ha seguito";
|
||||
"Scene.Notification.NotificationDescription.MentionedYou" = "ti ha menzionato";
|
||||
"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.Title.Everything" = "Tutto";
|
||||
"Scene.Notification.Title.Mentions" = "Menzioni";
|
||||
|
|
|
@ -297,38 +297,38 @@
|
|||
"Scene.Report.SkipToSend" = "コメントなしで送信";
|
||||
"Scene.Report.Step1" = "ステップ 1/2";
|
||||
"Scene.Report.Step2" = "ステップ 2/2";
|
||||
"Scene.Report.StepFinal.BlockUser" = "Block %@";
|
||||
"Scene.Report.StepFinal.BlockUser" = "%@をブロック";
|
||||
"Scene.Report.StepFinal.DontWantToSeeThis" = "Don’t 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 they’ve been blocked.";
|
||||
"Scene.Report.StepFinal.Unfollow" = "Unfollow";
|
||||
"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@";
|
||||
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed";
|
||||
"Scene.Report.StepFinal.Unfollow" = "フォロー解除";
|
||||
"Scene.Report.StepFinal.UnfollowUser" = "%@をフォロー解除";
|
||||
"Scene.Report.StepFinal.Unfollowed" = "フォロー解除しました";
|
||||
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you don’t like on Mastodon, you can remove the person from your experience.";
|
||||
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@";
|
||||
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "私たちが確認している間でも、あなたは%@さんに対して対応することができます。";
|
||||
"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.";
|
||||
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?";
|
||||
"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4";
|
||||
"Scene.Report.StepOne.IDontLikeIt" = "I don’t like it";
|
||||
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see";
|
||||
"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules";
|
||||
"Scene.Report.StepOne.ItsSomethingElse" = "It’s something else";
|
||||
"Scene.Report.StepOne.ItsSpam" = "It’s spam";
|
||||
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies";
|
||||
"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match";
|
||||
"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4";
|
||||
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "What's wrong with this account?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisPost" = "What's wrong with this post?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?";
|
||||
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules";
|
||||
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "その他に私たちに伝えておくべき事はありますか?";
|
||||
"Scene.Report.StepFour.Step4Of4" = "ステップ 4/4";
|
||||
"Scene.Report.StepOne.IDontLikeIt" = "興味がありません";
|
||||
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "見たくない内容の場合";
|
||||
"Scene.Report.StepOne.ItViolatesServerRules" = "サーバーのルールに違反しています";
|
||||
"Scene.Report.StepOne.ItsSomethingElse" = "その他";
|
||||
"Scene.Report.StepOne.ItsSpam" = "これはスパムです";
|
||||
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "悪意あるリンクや虚偽の情報、執拗な返信など";
|
||||
"Scene.Report.StepOne.SelectTheBestMatch" = "最も近いものを選んでください";
|
||||
"Scene.Report.StepOne.Step1Of4" = "ステップ 1/4";
|
||||
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "当てはまる選択肢がない場合";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisAccount" = "このアカウントのどこが問題ですか?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisPost" = "この投稿のどこが問題ですか?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "%@さんのどこが問題ですか?";
|
||||
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "ルールに違反しているのを見つけた場合";
|
||||
"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?";
|
||||
"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply";
|
||||
"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4";
|
||||
"Scene.Report.StepThree.Step3Of4" = "ステップ 3/4";
|
||||
"Scene.Report.StepTwo.IJustDon’tLikeIt" = "I just don’t like it";
|
||||
"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply";
|
||||
"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4";
|
||||
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?";
|
||||
"Scene.Report.StepTwo.Step2Of4" = "ステップ 2/4";
|
||||
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "どのルールに違反していますか?";
|
||||
"Scene.Report.TextPlaceholder" = "追加コメントを入力";
|
||||
"Scene.Report.Title" = "%@を通報";
|
||||
"Scene.Report.TitleReport" = "報告する";
|
||||
|
@ -369,7 +369,7 @@
|
|||
"Scene.ServerPicker.EmptyState.FindingServers" = "利用可能なサーバーの検索...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "なし";
|
||||
"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.Language" = "言語";
|
||||
"Scene.ServerPicker.Label.Users" = "ユーザー";
|
||||
|
|
|
@ -200,10 +200,10 @@ Ad d-yettwasali ɣef Mastodon.";
|
|||
"Scene.ConfirmEmail.OpenEmailApp.OpenEmailClient" = "Ldi amsaɣ n yimayl";
|
||||
"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.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.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.Hashtags" = "Ihacṭagen";
|
||||
"Scene.Discovery.Tabs.News" = "Isallen";
|
||||
|
@ -213,11 +213,11 @@ Ad d-yettwasali ɣef Mastodon.";
|
|||
"Scene.Favorite.Title" = "Ismenyifen-ik·im";
|
||||
"Scene.FavoritedBy.Title" = "Favorited By";
|
||||
"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.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.LogoLabel" = "Logo Button";
|
||||
"Scene.HomeTimeline.NavigationBarState.Accessibility.LogoLabel" = "Taqeffalt n ulugu";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "Tissufaɣ timaynutin";
|
||||
"Scene.HomeTimeline.NavigationBarState.Offline" = "Beṛṛa n tuqqna";
|
||||
"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.Username.DuplicatePrompt" = "Isem-ayi n umseqdac yettwaṭṭef yakan.";
|
||||
"Scene.Register.Input.Username.Placeholder" = "isem n useqdac";
|
||||
"Scene.Register.LetsGetYouSetUpOnDomain" = "Let’s get you set up on %@";
|
||||
"Scene.Register.LetsGetYouSetUpOnDomain" = "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.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.Step1" = "Aḥric 1 seg 2";
|
||||
"Scene.Report.Step2" = "Aḥric 2 seg 2";
|
||||
"Scene.Report.StepFinal.BlockUser" = "Block %@";
|
||||
"Scene.Report.StepFinal.DontWantToSeeThis" = "Don’t want to see this?";
|
||||
"Scene.Report.StepFinal.MuteUser" = "Mute %@";
|
||||
"Scene.Report.StepFinal.TheyWillNoLongerBeAbleToFollowOrSeeYourPostsButTheyCanSeeIfTheyveBeenBlocked" = "They will no longer be able to follow or see your posts, but they can see if they’ve been blocked.";
|
||||
"Scene.Report.StepFinal.Unfollow" = "Unfollow";
|
||||
"Scene.Report.StepFinal.UnfollowUser" = "Unfollow %@";
|
||||
"Scene.Report.StepFinal.Unfollowed" = "Unfollowed";
|
||||
"Scene.Report.StepFinal.WhenYouSeeSomethingYouDontLikeOnMastodonYouCanRemoveThePersonFromYourExperience." = "When you see something you don’t like on Mastodon, you can remove the person from your experience.";
|
||||
"Scene.Report.StepFinal.WhileWeReviewThisYouCanTakeActionAgainstUser" = "While we review this, you can take action against %@";
|
||||
"Scene.Report.StepFinal.YouWontSeeTheirPostsOrReblogsInYourHomeFeedTheyWontKnowTheyVeBeenMuted" = "You won’t see their posts or reblogs in your home feed. They won’t know they’ve been muted.";
|
||||
"Scene.Report.StepFour.IsThereAnythingElseWeShouldKnow" = "Is there anything else we should know?";
|
||||
"Scene.Report.StepFour.Step4Of4" = "Step 4 of 4";
|
||||
"Scene.Report.StepOne.IDontLikeIt" = "I don’t like it";
|
||||
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "It is not something you want to see";
|
||||
"Scene.Report.StepOne.ItViolatesServerRules" = "It violates server rules";
|
||||
"Scene.Report.StepOne.ItsSomethingElse" = "It’s something else";
|
||||
"Scene.Report.StepOne.ItsSpam" = "It’s spam";
|
||||
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Malicious links, fake engagement, or repetetive replies";
|
||||
"Scene.Report.StepOne.SelectTheBestMatch" = "Select the best match";
|
||||
"Scene.Report.StepOne.Step1Of4" = "Step 1 of 4";
|
||||
"Scene.Report.StepOne.TheIssueDoesNotFitIntoOtherCategories" = "The issue does not fit into other categories";
|
||||
"Scene.Report.StepFinal.BlockUser" = "Sewḥel %@";
|
||||
"Scene.Report.StepFinal.DontWantToSeeThis" = "Ur tebɣiḍ ara ad twaliḍ aya?";
|
||||
"Scene.Report.StepFinal.MuteUser" = "Sgugem %@";
|
||||
"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" = "Ur ṭṭafaṛ ara";
|
||||
"Scene.Report.StepFinal.UnfollowUser" = "Y•Teḥbes aḍfar n %@";
|
||||
"Scene.Report.StepFinal.Unfollowed" = "Y•Teḥbes aḍfar n";
|
||||
"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" = "Ideg nekkni nessenqad tuttra-inek•inem, tzemreḍ ad tḥadreḍ mgal %@";
|
||||
"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" = "Yella wayen-nniḍen i ilaqen ad t-nẓer?";
|
||||
"Scene.Report.StepFour.Step4Of4" = "Aḥric 4 seg 4";
|
||||
"Scene.Report.StepOne.IDontLikeIt" = "Ur ḥemmleɣ ara aya";
|
||||
"Scene.Report.StepOne.ItIsNotSomethingYouWantToSee" = "D ayen akk ur bɣiɣ ara ad waliɣ";
|
||||
"Scene.Report.StepOne.ItViolatesServerRules" = "Truẓi n yilugan n uqeddac";
|
||||
"Scene.Report.StepOne.ItsSomethingElse" = "Ɣef ssebba-nniḍen";
|
||||
"Scene.Report.StepOne.ItsSpam" = "D aspam";
|
||||
"Scene.Report.StepOne.MaliciousLinksFakeEngagementOrRepetetiveReplies" = "Yir iseɣwan, yir agman d tririyin i d-yettuɣalen";
|
||||
"Scene.Report.StepOne.SelectTheBestMatch" = "Fren amṣada akk igerrzen";
|
||||
"Scene.Report.StepOne.Step1Of4" = "Aḥric 1 seg 4";
|
||||
"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.WhatsWrongWithThisPost" = "What's wrong with this post?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "What's wrong with %@?";
|
||||
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "You are aware that it breaks specific rules";
|
||||
"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Are there any posts that back up this report?";
|
||||
"Scene.Report.StepThree.SelectAllThatApply" = "Select all that apply";
|
||||
"Scene.Report.StepThree.Step3Of4" = "Step 3 of 4";
|
||||
"Scene.Report.StepTwo.IJustDon’tLikeIt" = "I just don’t like it";
|
||||
"Scene.Report.StepTwo.SelectAllThatApply" = "Select all that apply";
|
||||
"Scene.Report.StepTwo.Step2Of4" = "Step 2 of 4";
|
||||
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Which rules are being violated?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisPost" = "Acu n wugur yellan d tsuffeɣt-a?";
|
||||
"Scene.Report.StepOne.WhatsWrongWithThisUsername" = "Acu n wugur yellan d %@?";
|
||||
"Scene.Report.StepOne.YouAreAwareThatItBreaksSpecificRules" = "Teẓriḍ y•tettruẓu kra n yilugan";
|
||||
"Scene.Report.StepThree.AreThereAnyPostsThatBackUpThisReport" = "Llant tsuffaɣ ara isdemren aneqqis-a?";
|
||||
"Scene.Report.StepThree.SelectAllThatApply" = "Fren akk tifrat ara yettusnasen";
|
||||
"Scene.Report.StepThree.Step3Of4" = "Aḥric 3 seg 4";
|
||||
"Scene.Report.StepTwo.IJustDon’tLikeIt" = "Ur ḥemmleɣ ara kan aya";
|
||||
"Scene.Report.StepTwo.SelectAllThatApply" = "Fren akk tifrat ara yettusnasen";
|
||||
"Scene.Report.StepTwo.Step2Of4" = "Aḥric 2 seg 4";
|
||||
"Scene.Report.StepTwo.WhichRulesAreBeingViolated" = "Acu n yilugan i yettwarẓan?";
|
||||
"Scene.Report.TextPlaceholder" = "Aru neɣ senteḍ iwenniten-nniḍen";
|
||||
"Scene.Report.Title" = "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.NoResults" = "Ulac igemmaḍ";
|
||||
"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.Language" = "TUTLAYT";
|
||||
"Scene.ServerPicker.Label.Users" = "ISEQDACEN";
|
||||
|
|
|
@ -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.NoResults" = "Encam tune";
|
||||
"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.Language" = "ZIMAN";
|
||||
"Scene.ServerPicker.Label.Users" = "BIKARHÊNER";
|
||||
|
|
|
@ -374,7 +374,7 @@
|
|||
"Scene.ServerPicker.EmptyState.FindingServers" = "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "ไม่มีผลลัพธ์";
|
||||
"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.Language" = "ภาษา";
|
||||
"Scene.ServerPicker.Label.Users" = "ผู้ใช้";
|
||||
|
|
|
@ -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.NoResults" = "Không có kết quả";
|
||||
"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.Language" = "NGÔN NGỮ";
|
||||
"Scene.ServerPicker.Label.Users" = "NGƯỜI DÙNG";
|
||||
|
|
|
@ -90,6 +90,7 @@ extension Mastodon.API.Notifications {
|
|||
public let sinceID: Mastodon.Entity.Status.ID?
|
||||
public let minID: Mastodon.Entity.Status.ID?
|
||||
public let limit: Int?
|
||||
public let types: [Mastodon.Entity.Notification.NotificationType]?
|
||||
public let excludeTypes: [Mastodon.Entity.Notification.NotificationType]?
|
||||
public let accountID: String?
|
||||
|
||||
|
@ -98,6 +99,7 @@ extension Mastodon.API.Notifications {
|
|||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||
minID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int? = nil,
|
||||
types: [Mastodon.Entity.Notification.NotificationType]? = nil,
|
||||
excludeTypes: [Mastodon.Entity.Notification.NotificationType]? = nil,
|
||||
accountID: String? = nil
|
||||
) {
|
||||
|
@ -105,6 +107,7 @@ extension Mastodon.API.Notifications {
|
|||
self.sinceID = sinceID
|
||||
self.minID = minID
|
||||
self.limit = limit
|
||||
self.types = types
|
||||
self.excludeTypes = excludeTypes
|
||||
self.accountID = accountID
|
||||
}
|
||||
|
@ -115,6 +118,11 @@ extension Mastodon.API.Notifications {
|
|||
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
|
||||
minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $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 {
|
||||
excludeTypes.forEach {
|
||||
items.append(URLQueryItem(name: "exclude_types[]", value: $0.rawValue))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -84,7 +84,7 @@ public struct RelationshipActionOptionSet: OptionSet {
|
|||
case .pending: return L10n.Common.Controls.Friendship.pending
|
||||
case .following: return L10n.Common.Controls.Friendship.following
|
||||
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 .suspended: return L10n.Common.Controls.Friendship.follow
|
||||
case .edit: return L10n.Common.Controls.Friendship.editInfo
|
||||
|
@ -116,6 +116,7 @@ public final class RelationshipViewModel {
|
|||
@Published public var isMuting = false
|
||||
@Published public var isBlocking = false
|
||||
@Published public var isBlockingBy = false
|
||||
@Published public var isSuspended = false
|
||||
|
||||
public init() {
|
||||
Publishers.CombineLatest3(
|
||||
|
@ -182,7 +183,7 @@ extension RelationshipViewModel {
|
|||
self.isMuting = optionSet.contains(.muting)
|
||||
self.isBlockingBy = optionSet.contains(.blockingBy)
|
||||
self.isBlocking = optionSet.contains(.blocking)
|
||||
|
||||
self.isSuspended = optionSet.contains(.suspended)
|
||||
|
||||
self.optionSet = optionSet
|
||||
}
|
||||
|
@ -203,7 +204,7 @@ extension RelationshipViewModel {
|
|||
public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet {
|
||||
let isMyself = user.id == me.id && user.domain == me.domain
|
||||
guard !isMyself else {
|
||||
return [.isMyself]
|
||||
return [.isMyself, .edit]
|
||||
}
|
||||
|
||||
let isProtected = user.locked
|
||||
|
@ -248,6 +249,10 @@ extension RelationshipViewModel {
|
|||
optionSet.insert(.blocking)
|
||||
}
|
||||
|
||||
if user.suspended {
|
||||
optionSet.insert(.suspended)
|
||||
}
|
||||
|
||||
return optionSet
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
|
1
Podfile
1
Podfile
|
@ -8,6 +8,7 @@ target 'Mastodon' do
|
|||
|
||||
# UI
|
||||
pod 'UITextField+Shake', '~> 1.2'
|
||||
pod 'XLPagerTabStrip', '~> 9.0.0'
|
||||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.4.0'
|
||||
|
|
|
@ -8,6 +8,7 @@ PODS:
|
|||
- Sourcery/CLI-Only (1.6.1)
|
||||
- SwiftGen (6.4.0)
|
||||
- "UITextField+Shake (1.2.1)"
|
||||
- XLPagerTabStrip (9.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- DateToolsSwift (~> 5.0.0)
|
||||
|
@ -17,6 +18,7 @@ DEPENDENCIES:
|
|||
- Sourcery (~> 1.6.1)
|
||||
- SwiftGen (~> 6.4.0)
|
||||
- "UITextField+Shake (~> 1.2)"
|
||||
- XLPagerTabStrip (~> 9.0.0)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
|
@ -26,6 +28,7 @@ SPEC REPOS:
|
|||
- Sourcery
|
||||
- SwiftGen
|
||||
- "UITextField+Shake"
|
||||
- XLPagerTabStrip
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Keys:
|
||||
|
@ -39,7 +42,8 @@ SPEC CHECKSUMS:
|
|||
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
|
||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
|
||||
|
||||
PODFILE CHECKSUM: 335d0ca70493d4c280d0f8fd7f26fe9be6a4e289
|
||||
PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>133</string>
|
||||
<string>138</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
|
|
Loading…
Reference in New Issue