diff --git a/AppShared/Info.plist b/AppShared/Info.plist
index bc0be73ac..094d6d538 100644
--- a/AppShared/Info.plist
+++ b/AppShared/Info.plist
@@ -15,8 +15,8 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
diff --git a/Documentation/Acknowledgments.md b/Documentation/Acknowledgments.md
index eab4b93f5..ff6dbc081 100644
--- a/Documentation/Acknowledgments.md
+++ b/Documentation/Acknowledgments.md
@@ -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)
diff --git a/Localization/StringsConvertor/input/fr.lproj/app.json b/Localization/StringsConvertor/input/fr.lproj/app.json
index 97a8566e9..f481d156b 100644
--- a/Localization/StringsConvertor/input/fr.lproj/app.json
+++ b/Localization/StringsConvertor/input/fr.lproj/app.json
@@ -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": {
diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json
index af5ee5d50..02c33f321 100644
--- a/Localization/StringsConvertor/input/gl.lproj/app.json
+++ b/Localization/StringsConvertor/input/gl.lproj/app.json
@@ -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",
diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json
index 9543837b5..57ed4f46b 100644
--- a/Localization/StringsConvertor/input/it.lproj/app.json
+++ b/Localization/StringsConvertor/input/it.lproj/app.json
@@ -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"
diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json
index a4e55aae5..e61b09753 100644
--- a/Localization/StringsConvertor/input/ja.lproj/app.json
+++ b/Localization/StringsConvertor/input/ja.lproj/app.json
@@ -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": {
diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json
index 8987669c0..fa2cac641 100644
--- a/Localization/StringsConvertor/input/kab.lproj/app.json
+++ b/Localization/StringsConvertor/input/kab.lproj/app.json
@@ -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": {
diff --git a/Localization/StringsConvertor/input/kmr.lproj/app.json b/Localization/StringsConvertor/input/kmr.lproj/app.json
index 65f891f93..cc4fc8947 100644
--- a/Localization/StringsConvertor/input/kmr.lproj/app.json
+++ b/Localization/StringsConvertor/input/kmr.lproj/app.json
@@ -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...",
diff --git a/Localization/StringsConvertor/input/th.lproj/app.json b/Localization/StringsConvertor/input/th.lproj/app.json
index 97a00b41a..dbd6153b0 100644
--- a/Localization/StringsConvertor/input/th.lproj/app.json
+++ b/Localization/StringsConvertor/input/th.lproj/app.json
@@ -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": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...",
diff --git a/Localization/StringsConvertor/input/vi.lproj/app.json b/Localization/StringsConvertor/input/vi.lproj/app.json
index 58650a088..62c1d240c 100644
--- a/Localization/StringsConvertor/input/vi.lproj/app.json
+++ b/Localization/StringsConvertor/input/vi.lproj/app.json
@@ -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...",
diff --git a/Localization/app.json b/Localization/app.json
index 7c50eba7f..2a8634a67 100644
--- a/Localization/app.json
+++ b/Localization/app.json
@@ -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...",
diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj
index 2187fa0c5..efe519f74 100644
--- a/Mastodon.xcodeproj/project.pbxproj
+++ b/Mastodon.xcodeproj/project.pbxproj
@@ -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 = ""; };
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; };
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = ""; };
+ DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = ""; };
+ DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = ""; };
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = ""; };
DB0FCB6B27950E29006C02E2 /* MastodonMentionContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMentionContainer.swift; sourceTree = ""; };
DB0FCB6D27950E6B006C02E2 /* MastodonMention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMention.swift; sourceTree = ""; };
@@ -1136,6 +1140,7 @@
DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceFacade.swift; sourceTree = ""; };
DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Profile.swift"; sourceTree = ""; };
DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Model.swift"; sourceTree = ""; };
+ DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagerTabStripNavigateable.swift; sourceTree = ""; };
DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; };
DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = ""; };
DB6B74EE272FB55000C70B6E /* FollowerListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerListViewController.swift; sourceTree = ""; };
@@ -1285,7 +1290,6 @@
DBB45B5A27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewTransitionViewController.swift; sourceTree = ""; };
DBB45B5F27B50A4F002DC5A7 /* RecommendAccountItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountItem.swift; sourceTree = ""; };
DBB45B6127B51112002DC5A7 /* SuggestionAccountViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountViewModel+Diffable.swift"; sourceTree = ""; };
- DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; };
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; };
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; };
DBB525352611ECEB002F1F29 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = ""; };
@@ -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 = "";
@@ -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 = "";
};
- DBB525132611EBB1002F1F29 /* Segmented */ = {
- isa = PBXGroup;
- children = (
- DBB525262611EBDA002F1F29 /* Paging */,
- DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */,
- );
- path = Segmented;
- sourceTree = "";
- };
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" */;
diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 3e2139aa3..1c922a0b5 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -9,7 +9,7 @@
isShown
orderHint
- 5
+ 9
CoreDataStack.xcscheme_^#shared#^_
@@ -24,22 +24,22 @@
Mastodon - RTL.xcscheme_^#shared#^_
orderHint
- 8
+ 11
Mastodon - Release.xcscheme_^#shared#^_
orderHint
- 2
+ 4
Mastodon - Snapshot.xcscheme_^#shared#^_
orderHint
- 3
+ 6
Mastodon - ar.xcscheme
orderHint
- 4
+ 8
Mastodon - ar.xcscheme_^#shared#^_
@@ -114,7 +114,7 @@
MastodonIntent.xcscheme_^#shared#^_
orderHint
- 28
+ 30
MastodonIntents.xcscheme_^#shared#^_
@@ -134,7 +134,7 @@
ShareActionExtension.xcscheme_^#shared#^_
orderHint
- 27
+ 31
SuppressBuildableAutocreation
diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
index cca51e911..29c81554a 100644
--- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -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",
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index 9df3040c7..4a4b43407 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -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):
diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist
index 66fdfcaaf..0407f6502 100644
--- a/Mastodon/Info.plist
+++ b/Mastodon/Info.plist
@@ -30,7 +30,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleURLTypes
@@ -43,7 +43,7 @@
CFBundleVersion
- 133
+ 138
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/Mastodon/Protocol/PagerTabStripNavigateable.swift b/Mastodon/Protocol/PagerTabStripNavigateable.swift
new file mode 100644
index 000000000..14ef8bbe6
--- /dev/null
+++ b/Mastodon/Protocol/PagerTabStripNavigateable.swift
@@ -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)
+ }
+}
+
diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift
index ce4e03cdb..c80121e98 100644
--- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift
+++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift
@@ -112,6 +112,12 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid
@MainActor
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,
diff --git a/Mastodon/Protocol/ScrollViewContainer.swift b/Mastodon/Protocol/ScrollViewContainer.swift
index c9f10ba3a..ae79d0e0f 100644
--- a/Mastodon/Protocol/ScrollViewContainer.swift
+++ b/Mastodon/Protocol/ScrollViewContainer.swift
@@ -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)
}
}
diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
index 2b480464d..96111d9f8 100644
--- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
+++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift
@@ -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
diff --git a/Mastodon/Scene/Account/View/BadgeButton.swift b/Mastodon/Scene/Account/View/BadgeButton.swift
index 785053be9..c4fd28e89 100644
--- a/Mastodon/Scene/Account/View/BadgeButton.swift
+++ b/Mastodon/Scene/Account/View/BadgeButton.swift
@@ -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)
diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift
index 4cc32c250..524805ad7 100644
--- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift
+++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewController.swift
@@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer
extension DiscoveryCommunityViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- tableView
- }
+ var scrollView: UIScrollView { tableView }
}
extension DiscoveryCommunityViewController {
diff --git a/Mastodon/Scene/Discovery/DiscoveryViewController.swift b/Mastodon/Scene/Discovery/DiscoveryViewController.swift
index 1803f687a..d94e6e592 100644
--- a/Mastodon/Scene/Discovery/DiscoveryViewController.swift
+++ b/Mastodon/Scene/Discovery/DiscoveryViewController.swift
@@ -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()
}
}
diff --git a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift
index c7b8fb7f5..9f6368e63 100644
--- a/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift
+++ b/Mastodon/Scene/Discovery/ForYou/DiscoveryForYouViewController.swift
@@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
// MARK: ScrollViewContainer
extension DiscoveryForYouViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- tableView
- }
+ var scrollView: UIScrollView { tableView }
}
diff --git a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift
index 6e6d96924..20ad408a2 100644
--- a/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift
+++ b/Mastodon/Scene/Discovery/Hashtags/DiscoveryHashtagsViewController.swift
@@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer
extension DiscoveryHashtagsViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- tableView
- }
+ var scrollView: UIScrollView { tableView }
}
extension DiscoveryHashtagsViewController {
diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift
index f73602ae4..d2415145c 100644
--- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift
+++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewController.swift
@@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer
extension DiscoveryNewsViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- tableView
- }
+ var scrollView: UIScrollView { tableView }
}
extension DiscoveryNewsViewController {
diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift
index a1d5b5e76..537ca1c58 100644
--- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift
+++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewController.swift
@@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer
extension DiscoveryPostsViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- tableView
- }
+ var scrollView: UIScrollView { tableView }
}
// MARK: - DiscoveryIntroBannerViewDelegate
diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
index 64d3d5941..871d47c28 100644
--- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
+++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift
@@ -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,
diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
index ae55134c4..e1e367e37 100644
--- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
+++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift
@@ -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)
}
}
diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift
index 16130251c..300b9165d 100644
--- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift
+++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift
@@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
// MARK: - ScrollViewContainer
extension NotificationTimelineViewController: ScrollViewContainer {
-
- var scrollView: UIScrollView? { tableView }
-
+ var scrollView: UIScrollView { tableView }
}
extension NotificationTimelineViewController {
diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift
index bc67a6304..5461223cb 100644
--- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift
+++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift
@@ -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
}
}
diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift
index ee2ac8a0e..c48ed1199 100644
--- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift
+++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift
@@ -88,7 +88,6 @@ extension NotificationTimelineViewModel {
}
}
-
var excludeTypes: [MastodonNotificationType]? {
switch self {
case .everything: return nil
diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift
index dd4d97047..0935c9967 100644
--- a/Mastodon/Scene/Notification/NotificationViewController.swift
+++ b/Mastodon/Scene/Notification/NotificationViewController.swift
@@ -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
}
diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift
index 4879be744..47385813d 100644
--- a/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift
+++ b/Mastodon/Scene/Profile/About/ProfileAboutViewController.swift
@@ -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)
}
}
diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel+Diffable.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel+Diffable.swift
index 259cad12d..0a11a71f6 100644
--- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel+Diffable.swift
+++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel+Diffable.swift
@@ -25,7 +25,8 @@ extension ProfileAboutViewModel {
profileFieldEditCollectionViewCellDelegate: profileFieldEditCollectionViewCellDelegate
)
)
-
+ self.diffableDataSource = diffableDataSource
+
diffableDataSource.reorderingHandlers.canReorderItem = { item -> Bool in
switch item {
case .editField: return true
@@ -42,22 +43,25 @@ 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()
+ 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)
.sink { [weak self] isEditing, displayFields, editingFields, emojiMeta in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
-
+
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
@@ -69,17 +73,17 @@ extension ProfileAboutViewModel {
return ProfileFieldItem.field(field: field)
}
}
-
+
if isEditing, fields.count < ProfileHeaderViewModel.maxProfileFieldCount {
items.append(.addEntry)
}
-
+
if !isEditing, items.isEmpty {
items.append(.noResult)
}
-
+
snapshot.appendItems(items, toSection: .main)
-
+
diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil)
}
.store(in: &disposeBag)
diff --git a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
index c7ef895dd..8498c6866 100644
--- a/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
+++ b/Mastodon/Scene/Profile/About/ProfileAboutViewModel.swift
@@ -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?
+ let profileInfo = ProfileInfo()
+ let profileInfoEditing = ProfileInfo()
- let displayProfileInfo = ProfileInfo()
- let editProfileInfo = ProfileInfo()
- let editProfileInfoDidInitialized = CurrentValueSubject(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,
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
index de6ad5415..f35ac6aa4 100644
--- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift
@@ -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()
- weak var delegate: ProfileHeaderViewControllerDelegate?
+ weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
+ weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
+ var disposeBag = Set()
var viewModel: ProfileHeaderViewModel!
+ weak var delegate: ProfileHeaderViewControllerDelegate?
+ weak var headerDelegate: TabBarPagerHeaderDelegate?
+
+ let mediaPreviewTransitionController = MediaPreviewTransitionController()
+
let titleView: DoubleTitleLabelNavigationBarTitleView = {
let titleView = DoubleTitleLabelNavigationBarTitleView()
titleView.titleLabel.textColor = .white
@@ -43,39 +53,8 @@ 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,137 +103,73 @@ 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)
.receive(on: DispatchQueue.main)
.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 = .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 = .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 { }
diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
index 8bdce2a6d..e28b250cf 100644
--- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
+++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift
@@ -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,39 +23,44 @@ final class ProfileHeaderViewModel {
// input
let context: AppContext
+ @Published var user: MastodonUser?
+ @Published var relationshipActionOptionSet: RelationshipActionOptionSet = .none
+
@Published var isEditing = false
- @Published var accountForEdit: Mastodon.Entity.Account?
- @Published var emojiMeta: MastodonContent.Emojis = [:]
+ @Published var isUpdating = false
- let viewDidAppear = CurrentValueSubject(false)
- let needsSetupBottomShadow = CurrentValueSubject(true)
- let needsFiledCollectionViewHidden = CurrentValueSubject(false)
- let isTitleViewContentOffsetSet = CurrentValueSubject(false)
+ @Published var accountForEdit: Mastodon.Entity.Account?
+
+// let needsFiledCollectionViewHidden = CurrentValueSubject(false)
// output
- let isTitleViewDisplaying = CurrentValueSubject(false)
- let displayProfileInfo = ProfileInfo()
- let editProfileInfo = ProfileInfo()
- let editProfileInfoDidInitialized = CurrentValueSubject(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
- 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()
- }
- .store(in: &disposeBag)
+ $accountForEdit
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] account in
+ guard let self = self else { return }
+ 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
}
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift
new file mode 100644
index 000000000..ac33227cc
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift
@@ -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)
+ }
+}
+
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift
new file mode 100644
index 000000000..808e1d7ba
--- /dev/null
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift
@@ -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()
+
+ let viewDidAppear = PassthroughSubject()
+
+ @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()
+ }
+}
diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
index 1a6e10537..7257333d0 100644
--- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
+++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift
@@ -38,8 +38,16 @@ final class ProfileHeaderView: UIView {
weak var delegate: ProfileHeaderViewDelegate?
var disposeBag = Set()
- 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 = {
let imageView = 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
@@ -310,7 +328,7 @@ extension ProfileHeaderView {
layoutMarginsGuide.trailingAnchor.constraint(equalTo: container.trailingAnchor),
container.bottomAnchor.constraint(equalTo: bottomAnchor),
])
-
+
// dashboardContainer: H - [ padding | statusDashboardView ]
let dashboardContainer = UIStackView()
dashboardContainer.axis = .horizontal
@@ -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)
diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift
new file mode 100644
index 000000000..bfbe45471
--- /dev/null
+++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift
@@ -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()
+ 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)
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift
new file mode 100644
index 000000000..9b9e78d98
--- /dev/null
+++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift
@@ -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)
+ }
+
+}
diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift
index 4c3f9820a..6a51ff599 100644
--- a/Mastodon/Scene/Profile/ProfileViewController.swift
+++ b/Mastodon/Scene/Profile/ProfileViewController.swift
@@ -14,11 +14,11 @@ import MastodonAsset
import MastodonLocalization
import MastodonUI
import CoreDataStack
-import Tabman
-import Pageboy
+import TabBarPager
+import XLPagerTabStrip
protocol ProfileViewModelEditable {
- func isEdited() -> Bool
+ var isEdited: Bool { get }
}
final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@@ -41,7 +41,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
private(set) lazy var settingBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(
image: Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate),
@@ -52,7 +52,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
private(set) lazy var shareBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(
image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate),
@@ -63,7 +63,7 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
private(set) lazy var favoriteBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(
image: Asset.ObjectsAndTools.star.image.withRenderingMode(.alwaysTemplate),
@@ -74,53 +74,55 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
private(set) lazy var replyBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrowshape.turn.up.left"), style: .plain, target: self, action: #selector(ProfileViewController.replyBarButtonItemPressed(_:)))
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
let moreMenuBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
return barButtonItem
}()
-
+
let refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.tintColor = .white
return refreshControl
}()
- let containerScrollView: UIScrollView = {
- let scrollView = UIScrollView()
- scrollView.scrollsToTop = false
- scrollView.showsVerticalScrollIndicator = false
- scrollView.preservesSuperviewLayoutMargins = true
- scrollView.delaysContentTouches = false
- return scrollView
- }()
-
- let overlayScrollView: UIScrollView = {
- let scrollView = UIScrollView()
- scrollView.showsVerticalScrollIndicator = false
- scrollView.backgroundColor = .clear
- scrollView.delaysContentTouches = false
- return scrollView
- }()
-
- private(set) lazy var profileSegmentedViewController = ProfileSegmentedViewController()
+ private(set) lazy var tabBarPagerController = TabBarPagerController()
+
private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = {
let viewController = ProfileHeaderViewController()
+ viewController.context = context
+ viewController.coordinator = coordinator
viewController.viewModel = ProfileHeaderViewModel(context: context)
return viewController
}()
- private var profileBannerImageViewLayoutConstraint: NSLayoutConstraint!
-
- private var contentOffsets: [Int: CGFloat] = [:]
- var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
+ private(set) lazy var profilePagingViewController: ProfilePagingViewController = {
+ let profilePagingViewController = ProfilePagingViewController()
+ profilePagingViewController.viewModel = {
+ let profilePagingViewModel = ProfilePagingViewModel(
+ postsUserTimelineViewModel: viewModel.postsUserTimelineViewModel,
+ repliesUserTimelineViewModel: viewModel.repliesUserTimelineViewModel,
+ mediaUserTimelineViewModel: viewModel.mediaUserTimelineViewModel,
+ profileAboutViewModel: viewModel.profileAboutViewModel
+ )
+ profilePagingViewModel.viewControllers.forEach { viewController in
+ if let viewController = viewController as? NeedsDependency {
+ viewController.context = context
+ viewController.coordinator = coordinator
+ }
+ }
+ return profilePagingViewModel
+ }()
+ return profilePagingViewController
+ }()
+
// title view nested in header
var titleView: DoubleTitleLabelNavigationBarTitleView {
profileHeaderViewController.titleView
@@ -132,44 +134,18 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi
}
-extension ProfileViewController {
-
- func observeTableViewContentSize(scrollView: UIScrollView) -> NSKeyValueObservation {
- updateOverlayScrollViewContentSize(scrollView: scrollView)
- return scrollView.observe(\.contentSize, options: .new) { scrollView, change in
- self.updateOverlayScrollViewContentSize(scrollView: scrollView)
- }
- }
-
- func updateOverlayScrollViewContentSize(scrollView: UIScrollView) {
- let bottomPageHeight = max(scrollView.contentSize.height, self.containerScrollView.frame.height - ProfileHeaderViewController.headerMinHeight - self.containerScrollView.safeAreaInsets.bottom)
- let headerViewHeight: CGFloat = profileHeaderViewController.view.frame.height
- let contentSize = CGSize(
- width: self.containerScrollView.contentSize.width,
- height: bottomPageHeight + headerViewHeight
- )
- self.overlayScrollView.contentSize = contentSize
- // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
- }
-
-}
-
extension ProfileViewController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
-
+
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
-
+
profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets)
}
- override var isViewLoaded: Bool {
- return super.isViewLoaded
- }
-
override func viewDidLoad() {
super.viewDidLoad()
@@ -191,21 +167,21 @@ extension ProfileViewController {
navigationItem.standardAppearance = barAppearance
navigationItem.compactAppearance = barAppearance
navigationItem.scrollEdgeAppearance = barAppearance
-
+
navigationItem.titleView = titleView
let editingAndUpdatingPublisher = Publishers.CombineLatest(
- viewModel.isEditing.eraseToAnyPublisher(),
- viewModel.isUpdating.eraseToAnyPublisher()
+ viewModel.$isEditing,
+ viewModel.$isUpdating
)
// note: not add .share() here
-
+
let barButtonItemHiddenPublisher = Publishers.CombineLatest3(
- viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(),
- viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(),
- viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher()
+ viewModel.$isMeBarButtonItemsHidden,
+ viewModel.$isReplyBarButtonItemHidden,
+ viewModel.$isMoreMenuBarButtonItemHidden
)
-
+
editingAndUpdatingPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] isEditing, isUpdating in
@@ -213,44 +189,44 @@ extension ProfileViewController {
self.cancelEditingBarButtonItem.isEnabled = !isUpdating
}
.store(in: &disposeBag)
-
+
Publishers.CombineLatest4 (
- viewModel.suspended.eraseToAnyPublisher(),
- profileHeaderViewController.viewModel.isTitleViewDisplaying.eraseToAnyPublisher(),
+ viewModel.relationshipViewModel.$isSuspended,
+ profileHeaderViewController.viewModel.$isTitleViewDisplaying,
editingAndUpdatingPublisher.eraseToAnyPublisher(),
barButtonItemHiddenPublisher.eraseToAnyPublisher()
)
.receive(on: DispatchQueue.main)
- .sink { [weak self] suspended, isTitleViewDisplaying, tuple1, tuple2 in
+ .sink { [weak self] isSuspended, isTitleViewDisplaying, tuple1, tuple2 in
guard let self = self else { return }
let (isEditing, _) = tuple1
let (isMeBarButtonItemsHidden, isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden) = tuple2
-
+
var items: [UIBarButtonItem] = []
defer {
self.navigationItem.rightBarButtonItems = !items.isEmpty ? items : nil
}
- guard !suspended else {
+ guard !isSuspended else {
return
}
-
+
guard !isEditing else {
items.append(self.cancelEditingBarButtonItem)
return
}
-
+
guard !isTitleViewDisplaying else {
return
}
-
+
guard isMeBarButtonItemsHidden else {
items.append(self.settingBarButtonItem)
items.append(self.shareBarButtonItem)
items.append(self.favoriteBarButtonItem)
return
}
-
+
if !isMoreMenuBarButtonItemHidden {
items.append(self.moreMenuBarButtonItem)
}
@@ -259,254 +235,90 @@ extension ProfileViewController {
}
}
.store(in: &disposeBag)
+
+ addChild(tabBarPagerController)
+ tabBarPagerController.view.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(tabBarPagerController.view)
+ tabBarPagerController.didMove(toParent: self)
+ NSLayoutConstraint.activate([
+ tabBarPagerController.view.topAnchor.constraint(equalTo: view.topAnchor),
+ tabBarPagerController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ tabBarPagerController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ tabBarPagerController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ ])
- overlayScrollView.refreshControl = refreshControl
+ tabBarPagerController.delegate = self
+ tabBarPagerController.dataSource = self
+
+ tabBarPagerController.relayScrollView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged)
-
- let postsUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: true))
- bind(userTimelineViewModel: postsUserTimelineViewModel)
-
- let repliesUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(excludeReplies: false))
- bind(userTimelineViewModel: repliesUserTimelineViewModel)
-
- let mediaUserTimelineViewModel = UserTimelineViewModel(context: context, domain: viewModel.domain.value, userID: viewModel.userID.value, queryFilter: UserTimelineViewModel.QueryFilter(onlyMedia: true))
- bind(userTimelineViewModel: mediaUserTimelineViewModel)
-
- let profileAboutViewModel = ProfileAboutViewModel(context: context)
-
- profileSegmentedViewController.pagingViewController.viewModel = {
- let profilePagingViewModel = ProfilePagingViewModel(
- postsUserTimelineViewModel: postsUserTimelineViewModel,
- repliesUserTimelineViewModel: repliesUserTimelineViewModel,
- mediaUserTimelineViewModel: mediaUserTimelineViewModel,
- profileAboutViewModel: profileAboutViewModel
- )
- profilePagingViewModel.viewControllers.forEach { viewController in
- if let viewController = viewController as? NeedsDependency {
- viewController.context = context
- viewController.coordinator = coordinator
- }
- }
- return profilePagingViewModel
- }()
-
- profileSegmentedViewController.pagingViewController.addBar(
- profileHeaderViewController.buttonBar,
- dataSource: profileSegmentedViewController.pagingViewController.viewModel,
- at: .custom(view: profileHeaderViewController.view, layout: { buttonBar in
- buttonBar.translatesAutoresizingMaskIntoConstraints = false
- self.profileHeaderViewController.view.addSubview(buttonBar)
- NSLayoutConstraint.activate([
- buttonBar.topAnchor.constraint(equalTo: self.profileHeaderViewController.profileHeaderView.bottomAnchor),
- buttonBar.leadingAnchor.constraint(equalTo: self.profileHeaderViewController.view.leadingAnchor),
- buttonBar.trailingAnchor.constraint(equalTo: self.profileHeaderViewController.view.trailingAnchor),
- buttonBar.bottomAnchor.constraint(equalTo: self.profileHeaderViewController.view.bottomAnchor),
- buttonBar.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.segmentedControlHeight).priority(.required - 1),
- ])
- })
- )
- updateBarButtonInsets()
-
- overlayScrollView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(overlayScrollView)
- NSLayoutConstraint.activate([
- overlayScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
- overlayScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- view.trailingAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.trailingAnchor),
- view.bottomAnchor.constraint(equalTo: overlayScrollView.frameLayoutGuide.bottomAnchor),
- overlayScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
- ])
-
- containerScrollView.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(containerScrollView)
- NSLayoutConstraint.activate([
- containerScrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor),
- containerScrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- view.trailingAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.trailingAnchor),
- view.bottomAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.bottomAnchor),
- containerScrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
- ])
-
- // add segmented list
- addChild(profileSegmentedViewController)
- profileSegmentedViewController.view.translatesAutoresizingMaskIntoConstraints = false
- containerScrollView.addSubview(profileSegmentedViewController.view)
- profileSegmentedViewController.didMove(toParent: self)
- NSLayoutConstraint.activate([
- profileSegmentedViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
- profileSegmentedViewController.view.trailingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.trailingAnchor),
- profileSegmentedViewController.view.bottomAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.bottomAnchor),
- profileSegmentedViewController.view.heightAnchor.constraint(equalTo: containerScrollView.frameLayoutGuide.heightAnchor),
- ])
-
- // add header
- addChild(profileHeaderViewController)
- profileHeaderViewController.view.translatesAutoresizingMaskIntoConstraints = false
- containerScrollView.addSubview(profileHeaderViewController.view)
- profileHeaderViewController.didMove(toParent: self)
- NSLayoutConstraint.activate([
- profileHeaderViewController.view.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
- profileHeaderViewController.view.leadingAnchor.constraint(equalTo: containerScrollView.contentLayoutGuide.leadingAnchor),
- containerScrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: profileHeaderViewController.view.trailingAnchor),
- profileSegmentedViewController.view.topAnchor.constraint(equalTo: profileHeaderViewController.view.bottomAnchor),
- ])
-
- containerScrollView.addGestureRecognizer(overlayScrollView.panGestureRecognizer)
- overlayScrollView.layer.zPosition = .greatestFiniteMagnitude // make vision top-most
- overlayScrollView.delegate = self
+
+ // setup delegate
profileHeaderViewController.delegate = self
- profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.delegate = self
- profileSegmentedViewController.pagingViewController.pagingDelegate = self
-
- // bind view model
- bindProfile(
- headerViewModel: profileHeaderViewController.viewModel,
- aboutViewModel: profileAboutViewModel
- )
-
+ profilePagingViewController.viewModel.profileAboutViewController.delegate = self
+
+ bindViewModel()
bindTitleView()
- bindHeader()
- bindProfileRelationship()
- bindProfileDashboard()
-
- viewModel.needsPagingEnabled
- .receive(on: DispatchQueue.main)
- .sink { [weak self] needsPaingEnabled in
- guard let self = self else { return }
- self.profileSegmentedViewController.pagingViewController.isScrollEnabled = needsPaingEnabled
- }
- .store(in: &disposeBag)
-
- profileHeaderViewController.profileHeaderView.delegate = self
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
-
- // set back button tint color in SceneCoordinator.present(scene:from:transition:)
-
- // force layout to make banner image tweak take effect
- view.layoutIfNeeded()
+ bindMoreBarButtonItem()
+ bindPager()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
- viewModel.viewDidAppear.send()
-
- // set overlay scroll view initial content size
- guard let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer,
- let scrollView = currentViewController.scrollView
- else { return }
-
- currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: scrollView)
- scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
+ setNeedsStatusBarAppearanceUpdate()
}
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
-
- currentPostTimelineTableViewContentSizeObservation = nil
- }
-
- override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- super.traitCollectionDidChange(previousTraitCollection)
-
- updateBarButtonInsets()
- }
-
}
extension ProfileViewController {
- private func updateBarButtonInsets() {
- let margin: CGFloat = {
- switch traitCollection.userInterfaceIdiom {
- case .phone:
- return ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
- default:
- return traitCollection.horizontalSizeClass == .regular ?
- ProfileViewController.containerViewMarginForRegularHorizontalSizeClass :
- ProfileViewController.containerViewMarginForCompactHorizontalSizeClass
- }
- }()
-
- profileHeaderViewController.buttonBar.layout.contentInset.left = margin
- profileHeaderViewController.buttonBar.layout.contentInset.right = margin
- }
-
-}
-
-extension ProfileViewController {
-
- private func bind(userTimelineViewModel: UserTimelineViewModel) {
- viewModel.domain.assign(to: \.domain, on: userTimelineViewModel).store(in: &disposeBag)
- viewModel.userID.assign(to: \.userID, on: userTimelineViewModel).store(in: &disposeBag)
- viewModel.isBlocking.assign(to: \.value, on: userTimelineViewModel.isBlocking).store(in: &disposeBag)
- viewModel.isBlockedBy.assign(to: \.value, on: userTimelineViewModel.isBlockedBy).store(in: &disposeBag)
- viewModel.suspended.assign(to: \.value, on: userTimelineViewModel.isSuspended).store(in: &disposeBag)
- viewModel.name.assign(to: \.value, on: userTimelineViewModel.userDisplayName).store(in: &disposeBag)
- }
-
- private func bindProfile(
- headerViewModel: ProfileHeaderViewModel,
- aboutViewModel: ProfileAboutViewModel
- ) {
+ private func bindViewModel() {
// header
- viewModel.avatarImageURL
- .receive(on: DispatchQueue.main)
- .assign(to: \.avatarImageURL, on: headerViewModel.displayProfileInfo)
+ let headerViewModel = profileHeaderViewController.viewModel!
+ viewModel.$user
+ .assign(to: \.user, on: headerViewModel)
.store(in: &disposeBag)
- viewModel.name
- .map { $0 ?? "" }
- .receive(on: DispatchQueue.main)
- .assign(to: \.name, on: headerViewModel.displayProfileInfo)
- .store(in: &disposeBag)
- viewModel.bioDescription
- .receive(on: DispatchQueue.main)
- .assign(to: \.note, on: headerViewModel.displayProfileInfo)
- .store(in: &disposeBag)
-
- // about
- Publishers.CombineLatest(
- viewModel.fields.removeDuplicates(),
- viewModel.emojiMeta.removeDuplicates()
- )
- .map { fields, emojiMeta -> [ProfileFieldItem.FieldValue] in
- fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value, emojiMeta: emojiMeta) }
- }
- .receive(on: DispatchQueue.main)
- .assign(to: \.fields, on: aboutViewModel.displayProfileInfo)
- .store(in: &disposeBag)
-
- // common
- viewModel.accountForEdit
- .assign(to: \.accountForEdit, on: headerViewModel)
- .store(in: &disposeBag)
- viewModel.accountForEdit
- .assign(to: \.accountForEdit, on: aboutViewModel)
- .store(in: &disposeBag)
- viewModel.emojiMeta
- .receive(on: DispatchQueue.main)
- .assign(to: \.emojiMeta, on: headerViewModel)
- .store(in: &disposeBag)
- viewModel.emojiMeta
- .receive(on: DispatchQueue.main)
- .assign(to: \.emojiMeta, on: aboutViewModel)
- .store(in: &disposeBag)
- viewModel.isEditing
+ viewModel.$isEditing
.assign(to: \.isEditing, on: headerViewModel)
.store(in: &disposeBag)
- viewModel.isEditing
+ viewModel.$isUpdating
+ .assign(to: \.isUpdating, on: headerViewModel)
+ .store(in: &disposeBag)
+ viewModel.relationshipViewModel.$optionSet
+ .map { $0 ?? .none }
+ .assign(to: \.relationshipActionOptionSet, on: headerViewModel)
+ .store(in: &disposeBag)
+ viewModel.$accountForEdit
+ .assign(to: \.accountForEdit, on: headerViewModel)
+ .store(in: &disposeBag)
+
+ // timeline
+ [
+ viewModel.postsUserTimelineViewModel,
+ viewModel.repliesUserTimelineViewModel,
+ viewModel.mediaUserTimelineViewModel,
+ ].forEach { userTimelineViewModel in
+ viewModel.relationshipViewModel.$isBlocking.assign(to: \.isBlocking, on: userTimelineViewModel).store(in: &disposeBag)
+ viewModel.relationshipViewModel.$isBlockingBy.assign(to: \.isBlockedBy, on: userTimelineViewModel).store(in: &disposeBag)
+ viewModel.relationshipViewModel.$isSuspended.assign(to: \.isSuspended, on: userTimelineViewModel).store(in: &disposeBag)
+ }
+
+ // about
+ let aboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel!
+ viewModel.$isEditing
.assign(to: \.isEditing, on: aboutViewModel)
.store(in: &disposeBag)
+ viewModel.$accountForEdit
+ .assign(to: \.accountForEdit, on: aboutViewModel)
+ .store(in: &disposeBag)
}
-
+
private func bindTitleView() {
Publishers.CombineLatest3(
- viewModel.name,
- viewModel.emojiMeta,
- viewModel.statusesCount
+ profileHeaderViewController.profileHeaderView.viewModel.$name,
+ profileHeaderViewController.profileHeaderView.viewModel.$emojiMeta,
+ profileHeaderViewController.profileHeaderView.viewModel.$statusesCount
)
.receive(on: DispatchQueue.main)
.sink { [weak self] name, emojiMeta, statusesCount in
@@ -527,7 +339,7 @@ extension ProfileViewController {
}
}
.store(in: &disposeBag)
- viewModel.name
+ profileHeaderViewController.profileHeaderView.viewModel.$name
.receive(on: DispatchQueue.main)
.sink { [weak self] name in
guard let self = self else { return }
@@ -535,99 +347,11 @@ extension ProfileViewController {
}
.store(in: &disposeBag)
}
-
- private func bindHeader() {
- // heaer UI
- Publishers.CombineLatest(
- viewModel.bannerImageURL.eraseToAnyPublisher(),
- viewModel.viewDidAppear.eraseToAnyPublisher()
- )
- .receive(on: DispatchQueue.main)
- .sink { [weak self] bannerImageURL, _ in
- guard let self = self else { return }
- self.profileHeaderViewController.profileHeaderView.bannerImageView.af.cancelImageRequest()
- let placeholder = UIImage.placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor)
- guard let bannerImageURL = bannerImageURL else {
- self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
- return
- }
- self.profileHeaderViewController.profileHeaderView.bannerImageView.af.setImage(
- withURL: bannerImageURL,
- placeholderImage: placeholder,
- imageTransition: .crossDissolve(0.3),
- runImageTransitionIfCached: false,
- completion: { [weak self] response in
- guard let self = self 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
- self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
- return
- }
- }
- )
- }
- .store(in: &disposeBag)
-
- viewModel.username
- .map { username in username.flatMap { "@" + $0 } ?? " " }
- .receive(on: DispatchQueue.main)
- .assign(to: \.text, on: profileHeaderViewController.profileHeaderView.usernameLabel)
- .store(in: &disposeBag)
-
- viewModel.isEditing
- .receive(on: DispatchQueue.main)
- .sink { [weak self] isEditing in
- guard let self = self else { return }
- // set first responder for key command
- if !isEditing {
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
- self.profileSegmentedViewController.pagingViewController.becomeFirstResponder()
- }
- }
-
- // dismiss keyboard if needs
- if !isEditing { self.view.endEditing(true) }
-
- self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isEditing
- if isEditing {
- // scroll to About page
- self.profileSegmentedViewController.pagingViewController.scrollToPage(
- .last,
- animated: true,
- completion: nil
- )
- self.profileSegmentedViewController.pagingViewController.isScrollEnabled = false
- } else {
- self.profileSegmentedViewController.pagingViewController.isScrollEnabled = true
- }
-
- let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
- animator.addAnimations {
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.alpha = isEditing ? 0.2 : 1.0
- }
- animator.startAnimation()
- }
- .store(in: &disposeBag)
-
- viewModel.needsImageOverlayBlurred
- .receive(on: DispatchQueue.main)
- .sink { [weak self] needsImageOverlayBlurred in
- guard let self = self else { return }
- UIView.animate(withDuration: 0.33) {
- let bannerEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.bannerImageViewOverlayBlurEffect : nil
- self.profileHeaderViewController.profileHeaderView.bannerImageViewOverlayVisualEffectView.effect = bannerEffect
- let avatarEffect: UIVisualEffect? = needsImageOverlayBlurred ? ProfileHeaderView.avatarImageViewOverlayBlurEffect : nil
- self.profileHeaderViewController.profileHeaderView.avatarImageViewOverlayVisualEffectView.effect = avatarEffect
- }
- }
- .store(in: &disposeBag)
- }
-
- private func bindProfileRelationship() {
+
+ private func bindMoreBarButtonItem() {
Publishers.CombineLatest(
viewModel.$user,
- viewModel.relationshipActionOptionSet
+ viewModel.relationshipViewModel.$optionSet
)
.asyncMap { [weak self] user, relationshipSet -> UIMenu? in
guard let self = self else { return nil }
@@ -638,8 +362,8 @@ extension ProfileViewController {
let _ = ManagedObjectRecord(objectID: user.objectID)
let menu = MastodonMenu.setupMenu(
actions: [
- .muteUser(.init(name: name, isMuting: self.viewModel.isMuting.value)),
- .blockUser(.init(name: name, isBlocking: self.viewModel.isBlocking.value)),
+ .muteUser(.init(name: name, isMuting: self.viewModel.relationshipViewModel.isMuting)),
+ .blockUser(.init(name: name, isBlocking: self.viewModel.relationshipViewModel.isBlocking)),
.reportUser(.init(name: name)),
.shareUser(.init(name: name)),
],
@@ -660,85 +384,62 @@ extension ProfileViewController {
self.moreMenuBarButtonItem.menu = menu
}
.store(in: &disposeBag)
-
- viewModel.isRelationshipActionButtonHidden
- .receive(on: DispatchQueue.main)
- .sink { [weak self] isHidden in
- guard let self = self else { return }
- self.profileHeaderViewController.profileHeaderView.relationshipActionButtonShadowContainer.isHidden = isHidden
- }
- .store(in: &disposeBag)
-
- Publishers.CombineLatest3(
- viewModel.relationshipActionOptionSet.eraseToAnyPublisher(),
- viewModel.isEditing.eraseToAnyPublisher(),
- viewModel.isUpdating.eraseToAnyPublisher()
- )
- .receive(on: DispatchQueue.main)
- .sink { [weak self] relationshipActionSet, isEditing, isUpdating in
- guard let self = self else { return }
- let friendshipButton = self.profileHeaderViewController.profileHeaderView.relationshipActionButton
- if relationshipActionSet.contains(.edit) {
- // check .edit state and set .editing when isEditing
- friendshipButton.configure(actionOptionSet: isUpdating ? .updating : (isEditing ? .editing : .edit))
- self.profileHeaderViewController.profileHeaderView.configure(state: isEditing ? .editing : .normal)
- } else {
- friendshipButton.configure(actionOptionSet: relationshipActionSet)
- }
- }
- .store(in: &disposeBag)
-
- Publishers.CombineLatest3(
- viewModel.isBlocking.eraseToAnyPublisher(),
- viewModel.isBlockedBy.eraseToAnyPublisher(),
- viewModel.suspended.eraseToAnyPublisher()
- )
- .receive(on: DispatchQueue.main)
- .sink { [weak self] isBlocking, isBlockedBy, suspended in
- guard let self = self else { return }
- let isNeedSetHidden = isBlocking || isBlockedBy || suspended
- self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden
- self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden
- self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden
- self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden
- self.viewModel.needsPagePinToTop.value = isNeedSetHidden
- }
- .store(in: &disposeBag)
- } // end func bindProfileRelationship
+ }
- private func bindProfileDashboard() {
- viewModel.statusesCount
+ private func bindPager() {
+ viewModel.$isPagingEnabled
.receive(on: DispatchQueue.main)
- .sink { [weak self] count in
+ .sink { [weak self] isPagingEnabled in
guard let self = self else { return }
- let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0)
+ self.profilePagingViewController.containerView.isScrollEnabled = isPagingEnabled
+ self.profilePagingViewController.buttonBarView.isUserInteractionEnabled = isPagingEnabled
}
.store(in: &disposeBag)
- viewModel.followingCount
+
+ viewModel.$isEditing
.receive(on: DispatchQueue.main)
- .sink { [weak self] count in
+ .sink { [weak self] isEditing in
guard let self = self else { return }
- let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.isAccessibilityElement = true
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.accessibilityLabel = L10n.Plural.Count.following(count ?? 0)
- }
- .store(in: &disposeBag)
- viewModel.followersCount
- .receive(on: DispatchQueue.main)
- .sink { [weak self] count in
- guard let self = self else { return }
- let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.isAccessibilityElement = true
- self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.accessibilityLabel = L10n.Plural.Count.follower(count ?? 0)
+ // set first responder for key command
+ if !isEditing {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ self.profilePagingViewController.becomeFirstResponder()
+ }
+ }
+
+ // dismiss keyboard if needs
+ if !isEditing { self.view.endEditing(true) }
+
+ if isEditing,
+ let index = self.profilePagingViewController.viewControllers.firstIndex(where: { type(of: $0) is ProfileAboutViewController.Type }),
+ self.profilePagingViewController.canMoveTo(index: index)
+ {
+ self.profilePagingViewController.moveToViewController(at: index)
+ }
}
.store(in: &disposeBag)
}
-
+
+// private func bindProfileRelationship() {
+//
+// Publishers.CombineLatest3(
+// viewModel.isBlocking.eraseToAnyPublisher(),
+// viewModel.isBlockedBy.eraseToAnyPublisher(),
+// viewModel.suspended.eraseToAnyPublisher()
+// )
+// .receive(on: DispatchQueue.main)
+// .sink { [weak self] isBlocking, isBlockedBy, suspended in
+// guard let self = self else { return }
+// let isNeedSetHidden = isBlocking || isBlockedBy || suspended
+// self.profileHeaderViewController.viewModel.needsSetupBottomShadow.value = !isNeedSetHidden
+// self.profileHeaderViewController.profileHeaderView.bioContainerView.isHidden = isNeedSetHidden
+// self.profileHeaderViewController.viewModel.needsFiledCollectionViewHidden.value = isNeedSetHidden
+// self.profileHeaderViewController.buttonBar.isUserInteractionEnabled = !isNeedSetHidden
+// self.viewModel.needsPagePinToTop.value = isNeedSetHidden
+// }
+// .store(in: &disposeBag)
+// } // end func bindProfileRelationship
+
private func handleMetaPress(_ meta: Meta) {
switch meta {
case .url(_, _, let url, _):
@@ -759,19 +460,19 @@ extension ProfileViewController {
}
extension ProfileViewController {
-
+
@objc private func cancelEditingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- viewModel.isEditing.value = false
+ viewModel.isEditing = false
}
-
+
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
-
+
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let user = viewModel.user else { return }
@@ -793,13 +494,13 @@ extension ProfileViewController {
)
} // end Task
}
-
+
@objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let favoriteViewModel = FavoriteViewModel(context: context)
coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show)
}
-
+
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
@@ -811,174 +512,185 @@ extension ProfileViewController {
)
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
-
+
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
- let currentViewController = profileSegmentedViewController.pagingViewController.currentViewController
- if let currentViewController = currentViewController as? UserTimelineViewController {
- currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
+ if let userTimelineViewController = profilePagingViewController.currentViewController as? UserTimelineViewController {
+ userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
sender.endRefreshing()
}
}
-
+
}
-// MARK: - UIScrollViewDelegate
-extension ProfileViewController: UIScrollViewDelegate {
+// MARK: - TabBarPagerDelegate
+extension ProfileViewController: TabBarPagerDelegate {
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
- let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
- if scrollView.contentOffset.y < topMaxContentOffsetY {
- self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
- for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
- postTimelineView.scrollView?.contentOffset.y = 0
- }
- contentOffsets.removeAll()
- } else {
- containerScrollView.contentOffset.y = topMaxContentOffsetY
- if viewModel.needsPagePinToTop.value {
- // do nothing
+ func tabBarMinimalHeight() -> CGFloat {
+ return ProfileHeaderViewController.headerMinHeight
+ }
+
+ func resetPageContentOffset(_ tabBarPagerController: TabBarPagerController) {
+ for viewController in profilePagingViewController.viewModel.viewControllers {
+ viewController.pageScrollView.contentOffset = .zero
+ }
+ }
+
+ func tabBarPagerController(_ tabBarPagerController: TabBarPagerController, didScroll scrollView: UIScrollView) {
+ // try to find some patterns:
+ // print("""
+ // -----
+ // headerMinHeight: \(ProfileHeaderViewController.headerMinHeight)
+ // scrollView.contentOffset.y: \(scrollView.contentOffset.y)
+ // scrollView.contentSize.height: \(scrollView.contentSize.height)
+ // scrollView.frame: \(scrollView.frame)
+ // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top)
+ // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom)
+ // """
+ // )
+
+
+ // elastically banner
+
+ // make banner top snap to window top
+ // do not rely on the view frame becase the header frame is .zero during the initial call
+ profileHeaderViewController.profileHeaderView.bannerImageViewTopLayoutConstraint.constant = min(0, scrollView.contentOffset.y)
+
+ if profileHeaderViewController.profileHeaderView.frame != .zero {
+ // make banner bottom not higher than navigation bar bottom
+ let bannerContainerInWindow = profileHeaderViewController.profileHeaderView.convert(
+ profileHeaderViewController.profileHeaderView.bannerContainerView.frame,
+ to: nil
+ )
+ let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
+ // print("bannerContainerBottomOffset: \(bannerContainerBottomOffset)")
+
+ let height = profileHeaderViewController.view.frame.height - bannerContainerInWindow.height
+ // make avata hidden when scroll 0.5x avatar height
+ let throttle = height != .zero ? 0.5 * ProfileHeaderView.avatarImageViewSize.height / height : 0
+ let progress: CGFloat
+
+ if bannerContainerBottomOffset < tabBarPagerController.containerScrollView.safeAreaInsets.top {
+ let offset = bannerContainerBottomOffset - tabBarPagerController.containerScrollView.safeAreaInsets.top
+ profileHeaderViewController.profileHeaderView.bannerImageViewBottomLayoutConstraint.constant = offset
+ // the progress for header move from banner bottom to header bottom (from 0 to 1)
+ progress = height != .zero ? abs(offset) / height : 0
} else {
- if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
- let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
- customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY
- }
+ profileHeaderViewController.profileHeaderView.bannerImageViewBottomLayoutConstraint.constant = 0
+ progress = 0
}
+ // setup titleView offset and fade avatar
+ profileHeaderViewController.updateHeaderScrollProgress(progress, throttle: throttle)
+
+ // setup buttonBar shadow
+ profilePagingViewController.updateButtonBarShadow(progress: progress)
}
-
- // elastically banner image
- let headerScrollProgress = (containerScrollView.contentOffset.y - containerScrollView.safeAreaInsets.top) / topMaxContentOffsetY
- let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY
- profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle)
}
-
+
}
+// MARK: - TabBarPagerDataSource
+extension ProfileViewController: TabBarPagerDataSource {
+ func headerViewController() -> UIViewController & TabBarPagerHeader {
+ return profileHeaderViewController
+ }
+
+ func pageViewController() -> UIViewController & TabBarPageViewController {
+ return profilePagingViewController
+ }
+}
+
+//// MARK: - UIScrollViewDelegate
+//extension ProfileViewController: UIScrollViewDelegate {
+//
+// func scrollViewDidScroll(_ scrollView: UIScrollView) {
+// contentOffsets[profileSegmentedViewController.pagingViewController.currentIndex!] = scrollView.contentOffset.y
+// let topMaxContentOffsetY = profileSegmentedViewController.view.frame.minY - ProfileHeaderViewController.headerMinHeight - containerScrollView.safeAreaInsets.top
+// if scrollView.contentOffset.y < topMaxContentOffsetY {
+// self.containerScrollView.contentOffset.y = scrollView.contentOffset.y
+// for postTimelineView in profileSegmentedViewController.pagingViewController.viewModel.viewControllers {
+// postTimelineView.scrollView?.contentOffset.y = 0
+// }
+// contentOffsets.removeAll()
+// } else {
+// containerScrollView.contentOffset.y = topMaxContentOffsetY
+// if viewModel.needsPagePinToTop.value {
+// // do nothing
+// } else {
+// if let customScrollViewContainerController = profileSegmentedViewController.pagingViewController.currentViewController as? ScrollViewContainer {
+// let contentOffsetY = scrollView.contentOffset.y - containerScrollView.contentOffset.y
+// customScrollViewContainerController.scrollView?.contentOffset.y = contentOffsetY
+// }
+// }
+//
+// }
+// }
+//
+//}
+
// MARK: - ProfileHeaderViewControllerDelegate
extension ProfileViewController: ProfileHeaderViewControllerDelegate {
-
- func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) {
- guard let scrollView = (profileSegmentedViewController.pagingViewController.currentViewController as? UserTimelineViewController)?.scrollView else {
- // assertionFailure()
- return
- }
-
- updateOverlayScrollViewContentSize(scrollView: scrollView)
- }
-
-}
-
-// MARK: - ProfilePagingViewControllerDelegate
-extension ProfileViewController: ProfilePagingViewControllerDelegate {
-
- func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController postTimelineViewController: ScrollViewContainer, atIndex index: Int) {
- os_log("%{public}s[%{public}ld], %{public}s: select at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
-
-// // update segemented control
-// if index < profileHeaderViewController.pageSegmentedControl.numberOfSegments {
-// profileHeaderViewController.pageSegmentedControl.selectedSegmentIndex = index
-// }
-
- // save content offset
- overlayScrollView.contentOffset.y = contentOffsets[index] ?? containerScrollView.contentOffset.y
-
- // setup observer and gesture fallback
- if let scrollView = postTimelineViewController.scrollView {
- currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: scrollView)
- scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
- }
- }
-
-}
-
-// MARK: - ProfileHeaderViewDelegate
-extension ProfileViewController: ProfileHeaderViewDelegate {
- func profileHeaderView(_ profileHeaderView: ProfileHeaderView, avatarButtonDidPressed button: AvatarButton) {
- guard let user = viewModel.user else { return }
- let record: ManagedObjectRecord = .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 = .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,
+ func profileHeaderViewController(
+ _ profileHeaderViewController: ProfileHeaderViewController,
+ profileHeaderView: ProfileHeaderView,
relationshipButtonDidPressed button: ProfileRelationshipActionButton
) {
- let relationshipActionSet = viewModel.relationshipActionOptionSet.value
-
+ let relationshipActionSet = viewModel.relationshipViewModel.optionSet ?? .none
+
// handle edit logic for editable profile
// handle relationship logic for non-editable profile
if relationshipActionSet.contains(.edit) {
// do nothing when updating
- guard !viewModel.isUpdating.value else { return }
-
+ guard !viewModel.isUpdating else { return }
+
guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return }
- guard let profileAboutViewModel = profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.viewModel else { return }
+ guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return }
- let isEdited = profileHeaderViewModel.isEdited()
- || profileAboutViewModel.isEdited()
+ let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited
if isEdited {
- // update profile if changed
- viewModel.isUpdating.value = true
- Task {
+ // update profile when edited
+ viewModel.isUpdating = true
+ Task { @MainActor in
do {
// TODO: handle error
_ = try await viewModel.updateProfileInfo(
- headerProfileInfo: profileHeaderViewModel.editProfileInfo,
- aboutProfileInfo: profileAboutViewModel.editProfileInfo
+ headerProfileInfo: profileHeaderViewModel.profileInfoEditing,
+ aboutProfileInfo: profileAboutViewModel.profileInfoEditing
)
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update profile info success")
- self.viewModel.isEditing.value = false
+ self.viewModel.isEditing = false
} catch {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update profile info fail: \(error.localizedDescription)")
+ let alertController = UIAlertController(
+ for: error,
+ title: L10n.Common.Alerts.EditProfileFailure.title,
+ preferredStyle: .alert
+ )
+ let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default)
+ alertController.addAction(okAction)
+ self.present(alertController, animated: true)
}
// finish updating
- self.viewModel.isUpdating.value = false
- }
+ self.viewModel.isUpdating = false
+ } // end Task
} else {
// set `updating` then toggle `edit` state
- viewModel.isUpdating.value = true
+ viewModel.isUpdating = true
viewModel.fetchEditProfileInfo()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
guard let self = self else { return }
defer {
// finish updating
- self.viewModel.isUpdating.value = false
+ self.viewModel.isUpdating = false
}
switch completion {
case .failure(let error):
@@ -994,11 +706,11 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
case .finished:
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit success", ((#file as NSString).lastPathComponent), #line, #function)
// enter editing mode
- self.viewModel.isEditing.value.toggle()
+ self.viewModel.isEditing.toggle()
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
- self.viewModel.accountForEdit.value = response.value
+ self.viewModel.accountForEdit = response.value
}
.store(in: &disposeBag)
}
@@ -1074,53 +786,27 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
assertionFailure()
}
}
+
}
-
- func profileHeaderView(_ profileHeaderView: ProfileHeaderView, metaTextView: MetaTextView, metaDidPressed meta: Meta) {
+
+ func profileHeaderViewController(
+ _ profileHeaderViewController: ProfileHeaderViewController,
+ profileHeaderView: ProfileHeaderView,
+ metaTextView: MetaTextView,
+ metaDidPressed meta: Meta
+ ) {
handleMetaPress(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.domain.value,
- let userID = viewModel.userID.value
- else { return }
- let followerListViewModel = FollowerListViewModel(
- context: context,
- domain: domain,
- userID: userID
- )
- coordinator.present(
- scene: .follower(viewModel: followerListViewModel),
- from: self,
- transition: .show
- )
- case .following:
- guard let domain = viewModel.domain.value,
- let userID = viewModel.userID.value
- else { return }
- let followingListViewModel = FollowingListViewModel(
- context: context,
- domain: domain,
- userID: userID
- )
- coordinator.present(
- scene: .following(viewModel: followingListViewModel),
- from: self,
- transition: .show
- )
- }
- }
-
}
// MARK: - ProfileAboutViewControllerDelegate
extension ProfileViewController: ProfileAboutViewControllerDelegate {
- func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) {
+ func profileAboutViewController(
+ _ viewController: ProfileAboutViewController,
+ profileFieldCollectionViewCell: ProfileFieldCollectionViewCell,
+ metaLabel: MetaLabel,
+ didSelectMeta meta: Meta
+ ) {
handleMetaPress(meta)
}
}
@@ -1130,9 +816,9 @@ extension ProfileViewController: MastodonMenuDelegate {
func menuAction(_ action: MastodonMenu.Action) {
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
guard let user = viewModel.user else { return }
-
+
let userRecord: ManagedObjectRecord = .init(objectID: user.objectID)
-
+
Task {
try await DataSourceFacade.responseToMenuAction(
dependency: self,
@@ -1151,16 +837,16 @@ extension ProfileViewController: MastodonMenuDelegate {
// MARK: - ScrollViewContainer
extension ProfileViewController: ScrollViewContainer {
- var scrollView: UIScrollView? {
- return overlayScrollView
+ var scrollView: UIScrollView {
+ return tabBarPagerController.containerScrollView
}
}
extension ProfileViewController {
override var keyCommands: [UIKeyCommand]? {
- if !viewModel.isEditing.value {
- return pageboyNavigateKeyCommands
+ if !viewModel.isEditing {
+ return pagerTabStripNavigateKeyCommands
}
return nil
@@ -1168,16 +854,16 @@ extension ProfileViewController {
}
-// MARK: - PageboyNavigateable
-extension ProfileViewController: PageboyNavigateable {
-
- var navigateablePageViewController: PageboyViewController {
- return profileSegmentedViewController.pagingViewController
+// MARK: - PagerTabStripNavigateable
+extension ProfileViewController: PagerTabStripNavigateable {
+
+ var navigateablePageViewController: PagerTabStripViewController {
+ return profilePagingViewController
}
-
- @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
- pageboyNavigateKeyCommandHandler(sender)
+
+ @objc func pagerTabStripNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
+ pagerTabStripNavigateKeyCommandHandler(sender)
}
-
+
}
diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift
index ac8c12e98..91866b851 100644
--- a/Mastodon/Scene/Profile/ProfileViewModel.swift
+++ b/Mastodon/Scene/Profile/ProfileViewModel.swift
@@ -27,97 +27,109 @@ 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()
+
+ @Published var isEditing = false
+ @Published var isUpdating = false
+ @Published var accountForEdit: Mastodon.Entity.Account?
// output
- let domain: CurrentValueSubject
- let userID: CurrentValueSubject
- let bannerImageURL: CurrentValueSubject
- let avatarImageURL: CurrentValueSubject
- let name: CurrentValueSubject
- let username: CurrentValueSubject
- let bioDescription: CurrentValueSubject
- let url: CurrentValueSubject
- let statusesCount: CurrentValueSubject
- let followingCount: CurrentValueSubject
- let followersCount: CurrentValueSubject
- let fields: CurrentValueSubject<[MastodonField], Never>
- let emojiMeta: CurrentValueSubject
-
- // fulfill this before editing
- let accountForEdit = CurrentValueSubject(nil)
-
- let protected: CurrentValueSubject
- let suspended: CurrentValueSubject
-
- let isEditing = CurrentValueSubject(false)
- let isUpdating = CurrentValueSubject(false)
+ let relationshipViewModel = RelationshipViewModel()
- let relationshipActionOptionSet = CurrentValueSubject(.none)
- let isFollowedBy = CurrentValueSubject(false)
- let isMuting = CurrentValueSubject(false)
- let isBlocking = CurrentValueSubject(false)
- let isBlockedBy = CurrentValueSubject(false)
+ @Published var userIdentifier: UserIdentifier? = nil
- let isRelationshipActionButtonHidden = CurrentValueSubject(true)
- let isReplyBarButtonItemHidden = CurrentValueSubject(true)
- let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true)
- let isMeBarButtonItemsHidden = CurrentValueSubject(true)
+ @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 needsPagePinToTop = CurrentValueSubject(false)
- let needsPagingEnabled = CurrentValueSubject(true)
- let needsImageOverlayBlurred = CurrentValueSubject(false)
+ // @Published var protected: Bool? = nil
+ // let needsPagePinToTop = CurrentValueSubject(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
- return
- }
- self.domain.value = authenticationBox.domain
- self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
+ 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
+ }
+
+ let isMyself = optionSet.contains(.isMyself)
+ self.isReplyBarButtonItemHidden = isMyself
+ self.isMoreMenuBarButtonItemHidden = isMyself
+ self.isMeBarButtonItemsHidden = !isMyself
+ }
+ .store(in: &disposeBag)
+
// query relationship
let userRecord = $user.map { user -> ManagedObjectRecord? in
user.flatMap { ManagedObjectRecord(objectID: $0.objectID) }
}
let pendingRetryPublisher = CurrentValueSubject(1)
-
+
// observe friendship
Publishers.CombineLatest3(
userRecord,
@@ -148,200 +160,25 @@ 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
- )
- .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
- }
+ Publishers.CombineLatest(
+ isBlockingOrBlocked,
+ $isEditing
+ )
+ .map { !$0 && !$1 }
+ .assign(to: &$isPagingEnabled)
}
-
+
}
extension ProfileViewModel {
@@ -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)
}
diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift
deleted file mode 100644
index 23630741f..000000000
--- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift
+++ /dev/null
@@ -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)
- }
-
-}
diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift
deleted file mode 100644
index 67a0ca93d..000000000
--- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift
+++ /dev/null
@@ -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]
- }
-}
diff --git a/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift b/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift
deleted file mode 100644
index 5d5241c56..000000000
--- a/Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift
+++ /dev/null
@@ -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),
- ])
- }
-
-}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
index d9e52a8c7..fb42b81b8 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift
@@ -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)
+ }
+}
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
index a0a1f52cd..7f7341aa6 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift
@@ -30,22 +30,19 @@ extension UserTimelineViewModel {
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
- // trigger user timeline loading
- Publishers.CombineLatest(
- $domain.removeDuplicates(),
- $userID.removeDuplicates()
- )
- .receive(on: DispatchQueue.main)
- .sink { [weak self] _ in
- guard let self = self else { return }
- self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
- }
- .store(in: &disposeBag)
+ // trigger timeline reloading
+ $userIdentifier
+ .receive(on: DispatchQueue.main)
+ .sink { [weak self] _ in
+ guard let self = self else { return }
+ self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
+ }
+ .store(in: &disposeBag)
let needsTimelineHidden = Publishers.CombineLatest3(
- isBlocking,
- isBlockedBy,
- isSuspended
+ $isBlocking,
+ $isBlockedBy,
+ $isSuspended
).map { $0 || $1 || $2 }
Publishers.CombineLatest(
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
index ae870f7b5..ca798fa0b 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift
@@ -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 }
diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
index 9701ba480..2d350fb0b 100644
--- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
+++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift
@@ -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(false)
- let isBlockedBy = CurrentValueSubject(false)
- let isSuspended = CurrentValueSubject(false)
- let userDisplayName = CurrentValueSubject(nil) // for suspended prompt label
- var dataSourceDidUpdate = PassthroughSubject()
+ @Published var isBlocking = false
+ @Published var isBlockedBy = false
+ @Published var isSuspended = false
+
+ // let userDisplayName = CurrentValueSubject(nil) // for suspended prompt label
+ // var dataSourceDidUpdate = PassthroughSubject()
// output
var diffableDataSource: UITableViewDiffableDataSource?
@@ -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
}
}
-
}
diff --git a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift
index ec145f86d..80df8938e 100644
--- a/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift
+++ b/Mastodon/Scene/Share/NavigationController/AdaptiveStatusBarStyleNavigationController.swift
@@ -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
}
diff --git a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift
index 717c35f82..99b0f3b6b 100644
--- a/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift
+++ b/Mastodon/Scene/Share/View/Content/PollOptionView+Configuration.swift
@@ -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)
}
diff --git a/Mastodon/Service/APIService/APIService+Notification.swift b/Mastodon/Service/APIService/APIService+Notification.swift
index 6cc0dbba3..921f75d01 100644
--- a/Mastodon/Service/APIService/APIService+Notification.swift
+++ b/Mastodon/Service/APIService/APIService+Notification.swift
@@ -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:
diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist
index 1a6ccd5fb..2f2b230f0 100644
--- a/MastodonIntent/Info.plist
+++ b/MastodonIntent/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
NSExtension
NSExtensionAttributes
diff --git a/MastodonSDK/Package.swift b/MastodonSDK/Package.swift
index c963be72d..aa349f8e0 100644
--- a/MastodonSDK/Package.swift
+++ b/MastodonSDK/Package.swift
@@ -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"),
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
index 1706ad363..b7a5071dd 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/fr.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
index d5ddb239d..a34df4ae8 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/gl.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
index 6e4e5589f..19e098e0f 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/it.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
index ea59763b1..fb4f5d9d6 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ja.lproj/Localizable.strings
@@ -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" = "ユーザー";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
index fc7c720ae..d9dc29100 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/kab.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
index 472bc9403..08f531c7e 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/ku.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
index 9ae1abc81..89c05abb6 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/th.lproj/Localizable.strings
@@ -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" = "ผู้ใช้";
diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
index 9978b2ea9..25b63dd88 100644
--- a/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
+++ b/MastodonSDK/Sources/MastodonLocalization/Resources/vi.lproj/Localizable.strings
@@ -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";
diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift
index c6b56c9e9..f70caaa1d 100644
--- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift
+++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift
@@ -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))
diff --git a/MastodonSDK/Sources/MastodonUI/DateTimeProvider.swift b/MastodonSDK/Sources/MastodonUI/Helper/DateTimeProvider.swift
similarity index 100%
rename from MastodonSDK/Sources/MastodonUI/DateTimeProvider.swift
rename to MastodonSDK/Sources/MastodonUI/Helper/DateTimeProvider.swift
diff --git a/MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift b/MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift
new file mode 100644
index 000000000..6db7499c6
--- /dev/null
+++ b/MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift
@@ -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
+ }
+}
diff --git a/MastodonSDK/Sources/MastodonUI/UserIdentifier.swift b/MastodonSDK/Sources/MastodonUI/UserIdentifier.swift
deleted file mode 100644
index ecde41d32..000000000
--- a/MastodonSDK/Sources/MastodonUI/UserIdentifier.swift
+++ /dev/null
@@ -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 }
-}
diff --git a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift
index cee31c14a..a19de5138 100644
--- a/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift
+++ b/MastodonSDK/Sources/MastodonUI/ViewModel/RelationshipViewModel.swift
@@ -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,8 +183,8 @@ 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
@@ -247,6 +248,10 @@ extension RelationshipViewModel {
if isBlocking {
optionSet.insert(.blocking)
}
+
+ if user.suspended {
+ optionSet.insert(.suspended)
+ }
return optionSet
}
diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist
index bc0be73ac..094d6d538 100644
--- a/MastodonTests/Info.plist
+++ b/MastodonTests/Info.plist
@@ -15,8 +15,8 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist
index bc0be73ac..094d6d538 100644
--- a/MastodonUITests/Info.plist
+++ b/MastodonUITests/Info.plist
@@ -15,8 +15,8 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist
index 31a95bf2b..ffeca43f4 100644
--- a/NotificationService/Info.plist
+++ b/NotificationService/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
NSExtension
NSExtensionPointIdentifier
diff --git a/Podfile b/Podfile
index 30f90a05e..a64cd0e55 100644
--- a/Podfile
+++ b/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'
diff --git a/Podfile.lock b/Podfile.lock
index 0c156eadc..629a48a87 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -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
diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist
index 7df1564a3..77557d6b7 100644
--- a/ShareActionExtension/Info.plist
+++ b/ShareActionExtension/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 133
+ 138
NSExtension
NSExtensionAttributes