From 820e1279c1355c3f0a3248a5e02a5ad3b9e7c5f5 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 23 May 2022 01:49:01 +0800 Subject: [PATCH 01/27] chore: update server search field placehoder --- Localization/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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...", From d8ab27a4c719ce1b22dbd060eccd90faa7954d6d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 May 2022 17:41:45 +0200 Subject: [PATCH 02/27] New translations app.json (Vietnamese) --- Localization/StringsConvertor/input/vi.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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...", From f4131ac7620d4ffe4fed8e70902bc4ee0b0b4faf Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 May 2022 18:40:27 +0200 Subject: [PATCH 03/27] New translations app.json (Thai) --- Localization/StringsConvertor/input/th.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": "กำลังค้นหาเซิร์ฟเวอร์ที่พร้อมใช้งาน...", From 2693546b07eac7a35027c21e1c733d6ebc257cf7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 May 2022 19:52:15 +0200 Subject: [PATCH 04/27] New translations app.json (Kurmanji (Kurdish)) --- Localization/StringsConvertor/input/kmr.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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...", From d30e432cbdb304304916d6f71a80aeba14b2a9e9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 09:12:36 +0200 Subject: [PATCH 05/27] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index 9543837b5..ca4df32de 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", @@ -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" From 08eb1bc5bf2ee4c9d5c0234209fd383b80e41900 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 10:11:18 +0200 Subject: [PATCH 06/27] New translations app.json (Galician) --- .../StringsConvertor/input/gl.lproj/app.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index af5ee5d50..9b9432602 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" + "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", From 46fc8d809c3043e1f881810264f143bce7ed65fc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 11:15:35 +0200 Subject: [PATCH 07/27] New translations app.json (Italian) --- Localization/StringsConvertor/input/it.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/it.lproj/app.json b/Localization/StringsConvertor/input/it.lproj/app.json index ca4df32de..57ed4f46b 100644 --- a/Localization/StringsConvertor/input/it.lproj/app.json +++ b/Localization/StringsConvertor/input/it.lproj/app.json @@ -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": { From 4045f1d0d4d34ccd624a5ecc249f5293f61ca427 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 11:15:36 +0200 Subject: [PATCH 08/27] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index a4e55aae5..d16f13312 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": "利用可能なサーバーの検索...", From 288851c8613ebeec05bb984f715c6408714989a8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 15:42:09 +0200 Subject: [PATCH 09/27] New translations app.json (French) --- Localization/StringsConvertor/input/fr.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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": { From 83d154c8282e92d4f5e5c116349fb5be2d23de8b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 15:42:10 +0200 Subject: [PATCH 10/27] New translations app.json (Japanese) --- Localization/StringsConvertor/input/ja.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index d16f13312..56f3ecb23 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -620,7 +620,7 @@ "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?", + "whats_wrong_with_this_username": "%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", From 15890e4c14b339b004f55520789a7b5d34cd7cfa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 24 May 2022 16:40:55 +0200 Subject: [PATCH 11/27] New translations app.json (Japanese) --- .../StringsConvertor/input/ja.lproj/app.json | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Localization/StringsConvertor/input/ja.lproj/app.json b/Localization/StringsConvertor/input/ja.lproj/app.json index 56f3ecb23..e61b09753 100644 --- a/Localization/StringsConvertor/input/ja.lproj/app.json +++ b/Localization/StringsConvertor/input/ja.lproj/app.json @@ -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": "%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": { From 503fcfab2a2c6ac30c00dc1d107e4d183aaa1124 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 13 May 2022 17:23:35 +0800 Subject: [PATCH 12/27] chore: [WIP] refactor profile UI --- Documentation/Acknowledgments.md | 1 + Mastodon.xcodeproj/project.pbxproj | 17 + .../xcshareddata/swiftpm/Package.resolved | 9 + Mastodon/Protocol/ScrollViewContainer.swift | 4 +- .../DiscoveryCommunityViewController.swift | 4 +- .../Discovery/DiscoveryViewController.swift | 4 +- .../DiscoveryForYouViewController.swift | 4 +- .../DiscoveryHashtagsViewController.swift | 4 +- .../News/DiscoveryNewsViewController.swift | 4 +- .../Posts/DiscoveryPostsViewController.swift | 4 +- .../HomeTimelineViewController.swift | 6 +- .../NotificationTimelineViewController.swift | 4 +- .../NotificationViewController.swift | 4 +- .../About/ProfileAboutViewController.swift | 17 +- .../Header/ProfileHeaderViewController.swift | 190 +- .../Scene/Profile/ProfileViewController.swift | 2021 +++++++++-------- Mastodon/Scene/Profile/ProfileViewModel.swift | 452 ++-- .../Paging/ProfilePagingViewController.swift | 74 +- .../Paging/ProfilePagingViewModel.swift | 71 +- .../Timeline/UserTimelineViewController.swift | 18 +- .../UserTimelineViewModel+Diffable.swift | 19 +- .../UserTimelineViewModel+State.swift | 4 +- .../Timeline/UserTimelineViewModel.swift | 19 +- .../{ => Helper}/DateTimeProvider.swift | 0 .../MastodonUI/Model/UserIdentifier.swift | 28 + .../Sources/MastodonUI/UserIdentifier.swift | 14 - Podfile | 1 + Podfile.lock | 6 +- 28 files changed, 1586 insertions(+), 1417 deletions(-) rename MastodonSDK/Sources/MastodonUI/{ => Helper}/DateTimeProvider.swift (100%) create mode 100644 MastodonSDK/Sources/MastodonUI/Model/UserIdentifier.swift delete mode 100644 MastodonSDK/Sources/MastodonUI/UserIdentifier.swift 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/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 2187fa0c5..f0bc7406a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -267,6 +267,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 */; }; @@ -1429,6 +1430,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 */, @@ -3448,6 +3450,7 @@ DBA5A52E26F07ED800CACBAA /* PanModal */, DB3EA911281BBEA800598866 /* AlamofireImage */, DB3EA913281BBEA800598866 /* Alamofire */, + DB486C0E282E41F200F69423 /* TabBarPager */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -3670,6 +3673,7 @@ DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */, + DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -5795,6 +5799,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"; @@ -5959,6 +5971,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.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index cca51e911..517917535 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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/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/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/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/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/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index de6ad5415..984ba44fb 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -15,7 +15,7 @@ import MastodonMeta import MetaTextKit import MastodonAsset import MastodonLocalization -import Tabman +import TabBarPager protocol ProfileHeaderViewControllerDelegate: AnyObject { func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) @@ -28,6 +28,7 @@ final class ProfileHeaderViewController: UIViewController { var disposeBag = Set() weak var delegate: ProfileHeaderViewControllerDelegate? + weak var headerDelegate: TabBarPagerHeaderDelegate? var viewModel: ProfileHeaderViewModel! @@ -44,35 +45,35 @@ 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 - }() +// 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 - } - } +// 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 @@ -113,7 +114,7 @@ extension ProfileHeaderViewController { override func viewDidLoad() { super.viewDidLoad() - customizeButtonBarAppearance() +// customizeButtonBarAppearance() view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor ThemeService.shared.currentTheme @@ -130,6 +131,7 @@ extension ProfileHeaderViewController { 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 @@ -262,14 +264,19 @@ extension ProfileHeaderViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view) - setupBottomShadow() + switch UIApplication.shared.applicationState { + case .active: + headerDelegate?.viewLayoutDidUpdate(self) + setupBottomShadow() + default: + break + } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - customizeButtonBarAppearance() +// customizeButtonBarAppearance() } } @@ -338,63 +345,63 @@ extension ProfileHeaderViewController { } } - 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 - } - - // set title view offset - let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) - let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y - 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 - - 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 - setProfileAvatar(alpha: alpha) - } else { - setProfileAvatar(alpha: 1) - } - } +// 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 +// } +// +// // set title view offset +// let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) +// let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y +// 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 +// +// 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 +// setProfileAvatar(alpha: alpha) +// } else { +// setProfileAvatar(alpha: 1) +// } +// } private func setProfileAvatar(alpha: CGFloat) { profileHeaderView.avatarImageViewBackgroundView.alpha = alpha @@ -488,3 +495,6 @@ extension ProfileHeaderViewController: CropViewControllerDelegate { cropViewController.dismiss(animated: true, completion: nil) } } + +// MARK: - TabBarPagerHeader +extension ProfileHeaderViewController: TabBarPagerHeader { } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 4c3f9820a..ec43f4d4a 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -14,8 +14,8 @@ import MastodonAsset import MastodonLocalization import MastodonUI import CoreDataStack -import Tabman -import Pageboy +import TabBarPager +import XLPagerTabStrip protocol ProfileViewModelEditable { func isEdited() -> Bool @@ -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,77 @@ 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.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 + }() + +// 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 var profileBannerImageViewLayoutConstraint: NSLayoutConstraint! +// private(set) lazy var profileSegmentedViewController = ProfilePagingViewController() +// +// private var contentOffsets: [Int: CGFloat] = [:] +// var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation? + // title view nested in header var titleView: DoubleTitleLabelNavigationBarTitleView { profileHeaderViewController.titleView @@ -132,43 +156,43 @@ 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 { +// +// 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 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,193 +215,210 @@ extension ProfileViewController { navigationItem.standardAppearance = barAppearance navigationItem.compactAppearance = barAppearance navigationItem.scrollEdgeAppearance = barAppearance - + navigationItem.titleView = titleView - let editingAndUpdatingPublisher = Publishers.CombineLatest( - viewModel.isEditing.eraseToAnyPublisher(), - viewModel.isUpdating.eraseToAnyPublisher() - ) - // note: not add .share() here - - let barButtonItemHiddenPublisher = Publishers.CombineLatest3( - viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(), - viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(), - viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher() - ) - - editingAndUpdatingPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] isEditing, isUpdating in - guard let self = self else { return } - self.cancelEditingBarButtonItem.isEnabled = !isUpdating - } - .store(in: &disposeBag) - - Publishers.CombineLatest4 ( - viewModel.suspended.eraseToAnyPublisher(), - profileHeaderViewController.viewModel.isTitleViewDisplaying.eraseToAnyPublisher(), - editingAndUpdatingPublisher.eraseToAnyPublisher(), - barButtonItemHiddenPublisher.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] suspended, 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 - } +// let editingAndUpdatingPublisher = Publishers.CombineLatest( +// viewModel.$isEditing, +// viewModel.$isUpdating +// ) +// // note: not add .share() here +// +// let barButtonItemHiddenPublisher = Publishers.CombineLatest3( +// viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(), +// viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(), +// viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher() +// ) +// +// editingAndUpdatingPublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] isEditing, isUpdating in +// guard let self = self else { return } +// self.cancelEditingBarButtonItem.isEnabled = !isUpdating +// } +// .store(in: &disposeBag) - guard !suspended 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) - } - if !isReplyBarButtonItemHidden { - items.append(self.replyBarButtonItem) - } - } - .store(in: &disposeBag) +// Publishers.CombineLatest4 ( +// viewModel.suspended.eraseToAnyPublisher(), +// profileHeaderViewController.viewModel.isTitleViewDisplaying.eraseToAnyPublisher(), +// editingAndUpdatingPublisher.eraseToAnyPublisher(), +// barButtonItemHiddenPublisher.eraseToAnyPublisher() +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] suspended, 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 { +// 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) +// } +// if !isReplyBarButtonItemHidden { +// items.append(self.replyBarButtonItem) +// } +// } +// .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 - profileHeaderViewController.delegate = self - profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.delegate = self - profileSegmentedViewController.pagingViewController.pagingDelegate = self - - // bind view model - bindProfile( - headerViewModel: profileHeaderViewController.viewModel, - aboutViewModel: profileAboutViewModel - ) - - 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 +// overlayScrollView.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 +// profileHeaderViewController.delegate = self +// profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.delegate = self +// profileSegmentedViewController.pagingViewController.pagingDelegate = self +// +// // bind view model +// bindProfile( +// headerViewModel: profileHeaderViewController.viewModel, +// aboutViewModel: profileAboutViewModel +// ) +// +// 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) { @@ -386,798 +427,834 @@ extension ProfileViewController { // set back button tint color in SceneCoordinator.present(scene:from:transition:) // force layout to make banner image tweak take effect - view.layoutIfNeeded() +// view.layoutIfNeeded() } 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) +// 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) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - currentPostTimelineTableViewContentSizeObservation = nil +// currentPostTimelineTableViewContentSizeObservation = nil } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - updateBarButtonInsets() +// 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 - } +// 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 - ) { - // header - viewModel.avatarImageURL - .receive(on: DispatchQueue.main) - .assign(to: \.avatarImageURL, on: headerViewModel.displayProfileInfo) - .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 - .assign(to: \.isEditing, on: headerViewModel) - .store(in: &disposeBag) - viewModel.isEditing - .assign(to: \.isEditing, on: aboutViewModel) - .store(in: &disposeBag) - } - - private func bindTitleView() { - Publishers.CombineLatest3( - viewModel.name, - viewModel.emojiMeta, - viewModel.statusesCount - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] name, emojiMeta, statusesCount in - guard let self = self else { return } - guard let title = name, let statusesCount = statusesCount, - let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { - self.titleView.isHidden = true - return - } - self.titleView.isHidden = false - let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) - let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) - do { - let metaContent = try MastodonMetaContent.convert(document: mastodonContent) - self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) - } catch { +// private func bind(userTimelineViewModel: UserTimelineViewModel) { +// viewModel.domain +// 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) +// } - } - } - .store(in: &disposeBag) - viewModel.name - .receive(on: DispatchQueue.main) - .sink { [weak self] name in - guard let self = self else { return } - self.navigationItem.title = name - } - .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() { - Publishers.CombineLatest( - viewModel.$user, - viewModel.relationshipActionOptionSet - ) - .asyncMap { [weak self] user, relationshipSet -> UIMenu? in - guard let self = self else { return nil } - guard let user = user else { - return nil - } - let name = user.displayNameWithFallback - 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)), - .reportUser(.init(name: name)), - .shareUser(.init(name: name)), - ], - delegate: self - ) - return menu - } - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure: - self.moreMenuBarButtonItem.menu = nil - case .finished: - break - } - } receiveValue: { [weak self] menu in - guard let self = self else { return } - 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 - .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.postDashboardMeterView.numberLabel.text = text - self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true - self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0) - } - .store(in: &disposeBag) - viewModel.followingCount - .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.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) - } - .store(in: &disposeBag) - } - - private func handleMetaPress(_ meta: Meta) { - switch meta { - case .url(_, _, let url, _): - guard let url = URL(string: url) else { return } - coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - case .mention(_, _, let userInfo): - guard let href = userInfo?["href"] as? String, - let url = URL(string: href) else { return } - coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) - case .hashtag(_, let hashtag, _): - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) - coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) - case .email, .emoji: - break - } - } +// private func bindProfile( +// headerViewModel: ProfileHeaderViewModel, +// aboutViewModel: ProfileAboutViewModel +// ) { +// // header +// viewModel.avatarImageURL +// .receive(on: DispatchQueue.main) +// .assign(to: \.avatarImageURL, on: headerViewModel.displayProfileInfo) +// .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 +// .assign(to: \.isEditing, on: headerViewModel) +// .store(in: &disposeBag) +// viewModel.isEditing +// .assign(to: \.isEditing, on: aboutViewModel) +// .store(in: &disposeBag) +// } +// +// private func bindTitleView() { +// Publishers.CombineLatest3( +// viewModel.name, +// viewModel.emojiMeta, +// viewModel.statusesCount +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] name, emojiMeta, statusesCount in +// guard let self = self else { return } +// guard let title = name, let statusesCount = statusesCount, +// let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { +// self.titleView.isHidden = true +// return +// } +// self.titleView.isHidden = false +// let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) +// let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) +// do { +// let metaContent = try MastodonMetaContent.convert(document: mastodonContent) +// self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) +// } catch { +// +// } +// } +// .store(in: &disposeBag) +// viewModel.name +// .receive(on: DispatchQueue.main) +// .sink { [weak self] name in +// guard let self = self else { return } +// self.navigationItem.title = name +// } +// .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() { +// Publishers.CombineLatest( +// viewModel.$user, +// viewModel.relationshipActionOptionSet +// ) +// .asyncMap { [weak self] user, relationshipSet -> UIMenu? in +// guard let self = self else { return nil } +// guard let user = user else { +// return nil +// } +// let name = user.displayNameWithFallback +// 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)), +// .reportUser(.init(name: name)), +// .shareUser(.init(name: name)), +// ], +// delegate: self +// ) +// return menu +// } +// .sink { [weak self] completion in +// guard let self = self else { return } +// switch completion { +// case .failure: +// self.moreMenuBarButtonItem.menu = nil +// case .finished: +// break +// } +// } receiveValue: { [weak self] menu in +// guard let self = self else { return } +// 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 +// .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.postDashboardMeterView.numberLabel.text = text +// self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true +// self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0) +// } +// .store(in: &disposeBag) +// viewModel.followingCount +// .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.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) +// } +// .store(in: &disposeBag) +// } +// +// private func handleMetaPress(_ meta: Meta) { +// switch meta { +// case .url(_, _, let url, _): +// guard let url = URL(string: url) else { return } +// coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) +// case .mention(_, _, let userInfo): +// guard let href = userInfo?["href"] as? String, +// let url = URL(string: href) else { return } +// coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) +// case .hashtag(_, let hashtag, _): +// let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) +// coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) +// case .email, .emoji: +// break +// } +// } } 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.value = 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)) +// 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 } - let record: ManagedObjectRecord = .init(objectID: user.objectID) - Task { - let _activityViewController = try await DataSourceFacade.createActivityViewController( - dependency: self, - user: record - ) - guard let activityViewController = _activityViewController else { return } - self.coordinator.present( - scene: .activityViewController( - activityViewController: activityViewController, - sourceView: nil, - barButtonItem: sender - ), - from: self, - transition: .activityViewControllerPresent(animated: true, completion: nil) - ) - } // end Task +// guard let user = viewModel.user else { return } +// let record: ManagedObjectRecord = .init(objectID: user.objectID) +// Task { +// let _activityViewController = try await DataSourceFacade.createActivityViewController( +// dependency: self, +// user: record +// ) +// guard let activityViewController = _activityViewController else { return } +// self.coordinator.present( +// scene: .activityViewController( +// activityViewController: activityViewController, +// sourceView: nil, +// barButtonItem: sender +// ), +// from: self, +// transition: .activityViewControllerPresent(animated: true, completion: nil) +// ) +// } // 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) +// 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 } - guard let mastodonUser = viewModel.user else { return } - let composeViewModel = ComposeViewModel( - context: context, - composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), - authenticationBox: authenticationBox - ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) +// guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } +// guard let mastodonUser = viewModel.user else { return } +// let composeViewModel = ComposeViewModel( +// context: context, +// composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), +// authenticationBox: authenticationBox +// ) +// 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) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - sender.endRefreshing() - } - } - -} - -// 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 - } - } - - } - - // elastically banner image - let headerScrollProgress = (containerScrollView.contentOffset.y - containerScrollView.safeAreaInsets.top) / topMaxContentOffsetY - let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY - profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle) - } - -} - -// 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 + let currentViewPage = profilePagingViewController.currentPage +// if let currentViewController = currentViewController as? UserTimelineViewController { +// currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) +// } +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { +// sender.endRefreshing() // } - - // 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 +// MARK: - TabBarPagerDelegate +extension ProfileViewController: TabBarPagerDelegate { + + func tabBarMinimalHeight() -> CGFloat { + return ProfileHeaderViewController.headerMinHeight } - 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 resetPageContentOffset(_ tabBarPagerController: TabBarPagerController) { + for viewController in profilePagingViewController.viewModel.viewControllers { + viewController.pageScrollView.contentOffset = .zero + } } - func profileHeaderView( - _ profileHeaderView: ProfileHeaderView, - relationshipButtonDidPressed button: ProfileRelationshipActionButton - ) { - let relationshipActionSet = viewModel.relationshipActionOptionSet.value - - // 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 let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } - guard let profileAboutViewModel = profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.viewModel else { return } - - let isEdited = profileHeaderViewModel.isEdited() - || profileAboutViewModel.isEdited() - - if isEdited { - // update profile if changed - viewModel.isUpdating.value = true - Task { - do { - // TODO: handle error - _ = try await viewModel.updateProfileInfo( - headerProfileInfo: profileHeaderViewModel.editProfileInfo, - aboutProfileInfo: profileAboutViewModel.editProfileInfo - ) - 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 - - } catch { - self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update profile info fail: \(error.localizedDescription)") - } - - // finish updating - self.viewModel.isUpdating.value = false - } - } else { - // set `updating` then toggle `edit` state - viewModel.isUpdating.value = 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 - } - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s", ((#file as NSString).lastPathComponent), #line, #function, 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, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - 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() - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.viewModel.accountForEdit.value = response.value - } - .store(in: &disposeBag) - } + func tabBarPagerController(_ tabBarPagerController: TabBarPagerController, didScroll scrollView: UIScrollView) { + // elastically banner + if scrollView.contentOffset.y < -scrollView.safeAreaInsets.top { + let offset = scrollView.contentOffset.y - (-scrollView.safeAreaInsets.top) + profileHeaderViewController.profileHeaderView.bannerImageViewTopLayoutConstraint.constant = offset } else { - guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } - switch relationshipAction { - case .none: - break - case .follow, .request, .pending, .following: - guard let user = viewModel.user else { return } - let reocrd = ManagedObjectRecord(objectID: user.objectID) - guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } - Task { - try await DataSourceFacade.responseToUserFollowAction( - dependency: self, - user: reocrd, - authenticationBox: authenticationBox - ) - } - case .muting: - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, - message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), - preferredStyle: .alert - ) - let record = ManagedObjectRecord(objectID: user.objectID) - let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in - guard let self = self else { return } - Task { - try await DataSourceFacade.responseToUserMuteAction( - dependency: self, - user: record, - authenticationBox: authenticationBox - ) - } - } - alertController.addAction(unmuteAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocking: - guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } - guard let user = viewModel.user else { return } - let name = user.displayNameWithFallback - - let alertController = UIAlertController( - title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, - message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), - preferredStyle: .alert - ) - let record = ManagedObjectRecord(objectID: user.objectID) - let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in - guard let self = self else { return } - Task { - try await DataSourceFacade.responseToUserBlockAction( - dependency: self, - user: record, - authenticationBox: authenticationBox - ) - } - } - alertController.addAction(unblockAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - case .blocked: - break - default: - assertionFailure() - } + profileHeaderViewController.profileHeaderView.bannerImageViewTopLayoutConstraint.constant = 0 } } - - func profileHeaderView(_ 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) { - handleMetaPress(meta) - } -} - -// MARK: - MastodonMenuDelegate -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, - action: action, - menuContext: DataSourceFacade.MenuContext( - author: userRecord, - status: nil, - button: nil, - barButtonItem: self.moreMenuBarButtonItem - ), - authenticationBox: authenticationBox - ) - } // end Task - } -} - -// MARK: - ScrollViewContainer -extension ProfileViewController: ScrollViewContainer { - var scrollView: UIScrollView? { - return overlayScrollView - } -} - -extension ProfileViewController { - - override var keyCommands: [UIKeyCommand]? { - if !viewModel.isEditing.value { - return pageboyNavigateKeyCommands - } - - return nil - } - -} - -// MARK: - PageboyNavigateable -extension ProfileViewController: PageboyNavigateable { - - var navigateablePageViewController: PageboyViewController { - return profileSegmentedViewController.pagingViewController +// MARK: - TabBarPagerDataSource +extension ProfileViewController: TabBarPagerDataSource { + func headerViewController() -> UIViewController & TabBarPagerHeader { + return profileHeaderViewController } - @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { - pageboyNavigateKeyCommandHandler(sender) + 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 +// } +// } +// +// } +// +// // elastically banner image +// let headerScrollProgress = (containerScrollView.contentOffset.y - containerScrollView.safeAreaInsets.top) / topMaxContentOffsetY +// let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY +// profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle) +// } +// +//} +// +//// 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, +// relationshipButtonDidPressed button: ProfileRelationshipActionButton +// ) { +// let relationshipActionSet = viewModel.relationshipActionOptionSet.value +// +// // 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 let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } +// guard let profileAboutViewModel = profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.viewModel else { return } +// +// let isEdited = profileHeaderViewModel.isEdited() +// || profileAboutViewModel.isEdited() +// +// if isEdited { +// // update profile if changed +// viewModel.isUpdating.value = true +// Task { +// do { +// // TODO: handle error +// _ = try await viewModel.updateProfileInfo( +// headerProfileInfo: profileHeaderViewModel.editProfileInfo, +// aboutProfileInfo: profileAboutViewModel.editProfileInfo +// ) +// 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 +// +// } catch { +// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update profile info fail: \(error.localizedDescription)") +// } +// +// // finish updating +// self.viewModel.isUpdating.value = false +// } +// } else { +// // set `updating` then toggle `edit` state +// viewModel.isUpdating.value = 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 +// } +// switch completion { +// case .failure(let error): +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s", ((#file as NSString).lastPathComponent), #line, #function, 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, handler: nil) +// alertController.addAction(okAction) +// self.coordinator.present( +// scene: .alertController(alertController: alertController), +// from: nil, +// transition: .alertController(animated: true, completion: nil) +// ) +// 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() +// } +// } receiveValue: { [weak self] response in +// guard let self = self else { return } +// self.viewModel.accountForEdit.value = response.value +// } +// .store(in: &disposeBag) +// } +// } else { +// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } +// switch relationshipAction { +// case .none: +// break +// case .follow, .request, .pending, .following: +// guard let user = viewModel.user else { return } +// let reocrd = ManagedObjectRecord(objectID: user.objectID) +// guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } +// Task { +// try await DataSourceFacade.responseToUserFollowAction( +// dependency: self, +// user: reocrd, +// authenticationBox: authenticationBox +// ) +// } +// case .muting: +// guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, +// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), +// preferredStyle: .alert +// ) +// let record = ManagedObjectRecord(objectID: user.objectID) +// let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in +// guard let self = self else { return } +// Task { +// try await DataSourceFacade.responseToUserMuteAction( +// dependency: self, +// user: record, +// authenticationBox: authenticationBox +// ) +// } +// } +// alertController.addAction(unmuteAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocking: +// guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } +// guard let user = viewModel.user else { return } +// let name = user.displayNameWithFallback +// +// let alertController = UIAlertController( +// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, +// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), +// preferredStyle: .alert +// ) +// let record = ManagedObjectRecord(objectID: user.objectID) +// let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in +// guard let self = self else { return } +// Task { +// try await DataSourceFacade.responseToUserBlockAction( +// dependency: self, +// user: record, +// authenticationBox: authenticationBox +// ) +// } +// } +// alertController.addAction(unblockAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) +// alertController.addAction(cancelAction) +// present(alertController, animated: true, completion: nil) +// case .blocked: +// break +// default: +// assertionFailure() +// } +// } +// } +// +// func profileHeaderView(_ 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) { +// handleMetaPress(meta) +// } +//} +// +//// MARK: - MastodonMenuDelegate +//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, +// action: action, +// menuContext: DataSourceFacade.MenuContext( +// author: userRecord, +// status: nil, +// button: nil, +// barButtonItem: self.moreMenuBarButtonItem +// ), +// authenticationBox: authenticationBox +// ) +// } // end Task +// } +//} +// +//// MARK: - ScrollViewContainer +//extension ProfileViewController: ScrollViewContainer { +// var scrollView: UIScrollView? { +// return overlayScrollView +// } +//} +// +//extension ProfileViewController { +// +// override var keyCommands: [UIKeyCommand]? { +// if !viewModel.isEditing.value { +// return pageboyNavigateKeyCommands +// } +// +// return nil +// } +// +//} +// +//// MARK: - PageboyNavigateable +//extension ProfileViewController: PageboyNavigateable { +// +// var navigateablePageViewController: PageboyViewController { +// return profileSegmentedViewController.pagingViewController +// } +// +// @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { +// pageboyNavigateKeyCommandHandler(sender) +// } +// +//} + diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index ac8c12e98..add87f312 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -27,149 +27,181 @@ 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 // 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 + @Published var userIdentifier: UserIdentifier? = nil + +// 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 protected: CurrentValueSubject +// let suspended: CurrentValueSubject - let isEditing = CurrentValueSubject(false) - let isUpdating = CurrentValueSubject(false) - - let relationshipActionOptionSet = CurrentValueSubject(.none) - let isFollowedBy = CurrentValueSubject(false) - let isMuting = CurrentValueSubject(false) - let isBlocking = CurrentValueSubject(false) - let isBlockedBy = CurrentValueSubject(false) - - let isRelationshipActionButtonHidden = CurrentValueSubject(true) - let isReplyBarButtonItemHidden = CurrentValueSubject(true) - let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true) - let isMeBarButtonItemsHidden = CurrentValueSubject(true) - - let needsPagePinToTop = CurrentValueSubject(false) - let needsPagingEnabled = CurrentValueSubject(true) - let needsImageOverlayBlurred = CurrentValueSubject(false) +// +// let relationshipActionOptionSet = CurrentValueSubject(.none) +// let isFollowedBy = CurrentValueSubject(false) +// let isMuting = CurrentValueSubject(false) +// let isBlocking = CurrentValueSubject(false) +// let isBlockedBy = CurrentValueSubject(false) +// +// let isRelationshipActionButtonHidden = CurrentValueSubject(true) +// let isReplyBarButtonItemHidden = CurrentValueSubject(true) +// let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true) +// let isMeBarButtonItemsHidden = CurrentValueSubject(true) +// +// let needsPagePinToTop = CurrentValueSubject(false) +// let needsPagingEnabled = CurrentValueSubject(true) +// let needsImageOverlayBlurred = 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.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, + queryFilter: .init(excludeReplies: true) + ) + self.repliesUserTimelineViewModel = UserTimelineViewModel( + context: context, + queryFilter: .init(excludeReplies: true) + ) + self.mediaUserTimelineViewModel = UserTimelineViewModel( + context: context, + 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) - - // query relationship - let userRecord = $user.map { user -> ManagedObjectRecord? in - user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } - } - let pendingRetryPublisher = CurrentValueSubject(1) - - // observe friendship - Publishers.CombineLatest3( - userRecord, - context.authenticationService.activeMastodonAuthenticationBox, - pendingRetryPublisher - ) - .sink { [weak self] userRecord, authenticationBox, _ in - guard let self = self else { return } - guard let userRecord = userRecord, - let authenticationBox = authenticationBox - else { return } - Task { - do { - let response = try await self.updateRelationship( - record: userRecord, - authenticationBox: authenticationBox - ) - // there are seconds delay after request follow before requested -> following. Query again when needs - guard let relationship = response.value.first else { return } - if relationship.requested == true { - let delay = pendingRetryPublisher.value - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - guard let _ = self else { return } - pendingRetryPublisher.value = min(2 * delay, 60) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function) - } - } - } 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)") - } + + // bind user + $user + .map { user -> UserIdentifier? in + guard let user = user else { return nil } + return MastodonUserIdentifier(domain: user.domain, userID: user.id) } - } - .store(in: &disposeBag) - - let isBlockingOrBlocked = Publishers.CombineLatest( - isBlocking, - isBlockedBy - ) - .map { $0 || $1 } - .share() + .assign(to: &$userIdentifier) + + $userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier) + $userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier) + $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier) + // $userIdentifier.assign(to: &profileAboutViewModel.$userIdentifier) + +// relationshipActionOptionSet +// .compactMap { $0.highPriorityAction(except: []) } +// .map { $0 == .none } +// .assign(to: \.value, on: isRelationshipActionButtonHidden) +// .store(in: &disposeBag) +// - isBlockingOrBlocked - .map { !$0 } - .assign(to: \.value, on: needsPagingEnabled) - .store(in: &disposeBag) - - isBlockingOrBlocked - .map { $0 } - .assign(to: \.value, on: needsImageOverlayBlurred) - .store(in: &disposeBag) - - setup() +// +// // query relationship +// let userRecord = $user.map { user -> ManagedObjectRecord? in +// user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } +// } +// let pendingRetryPublisher = CurrentValueSubject(1) +// +// // observe friendship +// Publishers.CombineLatest3( +// userRecord, +// context.authenticationService.activeMastodonAuthenticationBox, +// pendingRetryPublisher +// ) +// .sink { [weak self] userRecord, authenticationBox, _ in +// guard let self = self else { return } +// guard let userRecord = userRecord, +// let authenticationBox = authenticationBox +// else { return } +// Task { +// do { +// let response = try await self.updateRelationship( +// record: userRecord, +// authenticationBox: authenticationBox +// ) +// // there are seconds delay after request follow before requested -> following. Query again when needs +// guard let relationship = response.value.first else { return } +// if relationship.requested == true { +// let delay = pendingRetryPublisher.value +// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in +// guard let _ = self else { return } +// pendingRetryPublisher.value = min(2 * delay, 60) +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function) +// } +// } +// } 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)") +// } +// } +// } +// .store(in: &disposeBag) +// +// let isBlockingOrBlocked = Publishers.CombineLatest( +// isBlocking, +// isBlockedBy +// ) +// .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() } } @@ -245,101 +277,101 @@ extension ProfileViewModel { } 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 ?? [:] +// 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 - } +// 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 +// } } } diff --git a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift index 23630741f..4eca4268e 100644 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift +++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift @@ -7,40 +7,42 @@ import os.log import UIKit -import Pageboy -import Tabman +import XLPagerTabStrip +import TabBarPager protocol ProfilePagingViewControllerDelegate: AnyObject { func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int) } -final class ProfilePagingViewController: TabmanViewController { +final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, TabBarPageViewController { + weak var tabBarPageViewDelegate: TabBarPageViewDelegate? weak var pagingDelegate: ProfilePagingViewControllerDelegate? + var viewModel: ProfilePagingViewModel! + // MARK: - TabBarPageViewController - // 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) - } - } + var currentPage: TabBarPage? { + return viewModel.viewControllers[currentIndex] } - override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { - super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated) + 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) - let viewController = viewModel.viewControllers[index] - (viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top - pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index) + guard indexWasChanged else { return } + let page = viewModel.viewControllers[toIndex] + tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex) } // make key commands works @@ -49,7 +51,7 @@ final class ProfilePagingViewController: TabmanViewController { } 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) } } @@ -59,8 +61,8 @@ extension ProfilePagingViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .clear - dataSource = viewModel +// view.backgroundColor = .clear +// dataSource = viewModel } override func viewDidAppear(_ animated: Bool) { @@ -74,17 +76,17 @@ extension ProfilePagingViewController { // 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) - } +// override var keyCommands: [UIKeyCommand]? { +// return currentPage?.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 index 67a0ca93d..f8174acde 100644 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift @@ -7,10 +7,9 @@ import os.log import UIKit -import Pageboy -import Tabman import MastodonAsset import MastodonLocalization +import TabBarPager final class ProfilePagingViewModel: NSObject { @@ -32,7 +31,7 @@ final class ProfilePagingViewModel: NSObject { super.init() } - var viewControllers: [ScrollViewContainer] { + var viewControllers: [UIViewController & TabBarPage] { return [ postUserTimelineViewController, repliesUserTimelineViewController, @@ -41,42 +40,42 @@ final class ProfilePagingViewModel: NSObject { ] } - 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 - }() +// 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) + os_log(.info, log: .debug, "%{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] - } -} +//// 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/Timeline/UserTimelineViewController.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift index d9e52a8c7..54f936f75 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: "Hello") + } +} diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index a0a1f52cd..0fc3368d1 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -30,17 +30,14 @@ 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, diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index ae870f7b5..71213489f 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 } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift index 9701ba480..8882990ea 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -19,8 +19,7 @@ final class UserTimelineViewModel { // input let context: AppContext - @Published var domain: String? - @Published var userID: String? + @Published var userIdentifier: UserIdentifier? @Published var queryFilter: QueryFilter let statusFetchedResultsController: StatusFetchedResultsController let listBatchFetchViewModel = ListBatchFetchViewModel() @@ -48,30 +47,25 @@ final class UserTimelineViewModel { init( context: AppContext, - domain: String?, - userID: String?, queryFilter: QueryFilter ) { self.context = context 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 +86,4 @@ extension UserTimelineViewModel { self.onlyMedia = onlyMedia } } - } 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/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 From 95651c9e8e4856f5d9f8dab52a6073b8fc25e24d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 26 May 2022 06:36:12 +0200 Subject: [PATCH 13/27] New translations app.json (Galician) --- Localization/StringsConvertor/input/gl.lproj/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Localization/StringsConvertor/input/gl.lproj/app.json b/Localization/StringsConvertor/input/gl.lproj/app.json index 9b9432602..02c33f321 100644 --- a/Localization/StringsConvertor/input/gl.lproj/app.json +++ b/Localization/StringsConvertor/input/gl.lproj/app.json @@ -470,7 +470,7 @@ "footer": "Non se mostran os seguimentos desde outros servidores." }, "familiarFollowers": { - "title": "Followers you familiar", + "title": "Seguimentos próximos", "followed_by_names": "Seguimentos de %s" }, "favorited_by": { From ad63c512df0d00ce9e43aed16e041269c1232516 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 26 May 2022 23:19:47 +0800 Subject: [PATCH 14/27] fix: refactor the profile UI to fix internal AutoLayout crash issue. resolve #440 --- Mastodon.xcodeproj/project.pbxproj | 22 +- .../xcschemes/xcschememanagement.plist | 8 +- Mastodon/Coordinator/SceneCoordinator.swift | 1 + .../MediaPreviewViewController.swift | 12 + .../ProfileAboutViewModel+Diffable.swift | 24 +- .../Profile/About/ProfileAboutViewModel.swift | 63 +- .../Header/ProfileHeaderViewController.swift | 416 +++-- .../Header/ProfileHeaderViewModel.swift | 88 +- .../ProfileHeaderView+Configuration.swift | 56 + .../View/ProfileHeaderView+ViewModel.swift | 280 ++++ .../Header/View/ProfileHeaderView.swift | 98 +- .../Paging/ProfilePagingViewController.swift | 217 +++ .../Paging/ProfilePagingViewModel.swift | 37 +- .../Scene/Profile/ProfileViewController.swift | 1456 +++++++---------- Mastodon/Scene/Profile/ProfileViewModel.swift | 365 +---- .../Paging/ProfilePagingViewController.swift | 92 -- .../ProfileSegmentedViewController.swift | 39 - .../Timeline/UserTimelineViewController.swift | 2 +- .../UserTimelineViewModel+Diffable.swift | 6 +- .../UserTimelineViewModel+State.swift | 2 +- .../Timeline/UserTimelineViewModel.swift | 18 +- ...veStatusBarStyleNavigationController.swift | 4 +- .../ViewModel/RelationshipViewModel.swift | 11 +- 23 files changed, 1553 insertions(+), 1764 deletions(-) create mode 100644 Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift create mode 100644 Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift create mode 100644 Mastodon/Scene/Profile/Paging/ProfilePagingViewController.swift rename Mastodon/Scene/Profile/{Segmented => }/Paging/ProfilePagingViewModel.swift (56%) delete mode 100644 Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift delete mode 100644 Mastodon/Scene/Profile/Segmented/ProfileSegmentedViewController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f0bc7406a..b3ab9234b 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 */; }; @@ -508,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 */; }; @@ -889,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 = ""; }; @@ -1286,7 +1289,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 = ""; }; @@ -3006,8 +3008,8 @@ DB9D6C0825E4F5A60051B173 /* Profile */ = { isa = PBXGroup; children = ( - DBB525132611EBB1002F1F29 /* Segmented */, DBB525462611ED57002F1F29 /* Header */, + DBB525262611EBDA002F1F29 /* Paging */, DBB5253B2611ECF5002F1F29 /* Timeline */, DBE3CDF1261C6B3100430CC6 /* Favorite */, DB6B74F0272FB55400C70B6E /* Follower */, @@ -3106,15 +3108,6 @@ path = Video; sourceTree = ""; }; - DBB525132611EBB1002F1F29 /* Segmented */ = { - isa = PBXGroup; - children = ( - DBB525262611EBDA002F1F29 /* Paging */, - DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */, - ); - path = Segmented; - sourceTree = ""; - }; DBB525262611EBDA002F1F29 /* Paging */ = { isa = PBXGroup; children = ( @@ -3150,6 +3143,8 @@ isa = PBXGroup; children = ( DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */, + DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */, + DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */, DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */, ); path = View; @@ -4041,7 +4036,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 */, @@ -4398,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 */, @@ -4406,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 */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 3e2139aa3..6817fbe8e 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -24,7 +24,7 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 8 + 7 Mastodon - Release.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 28 + 23 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 29 + 22 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 27 + 24 SuppressBuildableAutocreation 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/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/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 984ba44fb..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 @@ -18,19 +19,27 @@ import MastodonLocalization 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 + 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? - var viewModel: ProfileHeaderViewModel! + let mediaPreviewTransitionController = MediaPreviewTransitionController() let titleView: DoubleTitleLabelNavigationBarTitleView = { let titleView = DoubleTitleLabelNavigationBarTitleView() @@ -44,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 @@ -104,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) } } @@ -114,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 @@ -125,6 +103,7 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) +// profileHeaderView.preservesSuperviewLayoutMargins = true profileHeaderView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(profileHeaderView) NSLayoutConstraint.activate([ @@ -133,130 +112,64 @@ extension ProfileHeaderViewController { 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() } @@ -264,19 +177,7 @@ extension ProfileHeaderViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - switch UIApplication.shared.applicationState { - case .active: - headerDelegate?.viewLayoutDidUpdate(self) - setupBottomShadow() - default: - break - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - -// customizeButtonBarAppearance() + headerDelegate?.viewLayoutDidUpdate(self) } } @@ -328,80 +229,28 @@ 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) + + // set title view offset + let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) + let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y + let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset + let transformY = max(0, titleViewContentOffset) + titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY) + viewModel.isTitleViewDisplaying = transformY < titleView.containerView.frame.height + viewModel.isTitleViewContentOffsetSet = true + + 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) } } - -// 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 -// } -// -// // set title view offset -// let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) -// let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y -// 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 -// -// 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 -// setProfileAvatar(alpha: alpha) -// } else { -// setProfileAvatar(alpha: 1) -// } -// } private func setProfileAvatar(alpha: CGFloat) { profileHeaderView.avatarImageViewBackgroundView.alpha = alpha @@ -411,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? { @@ -419,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: @@ -491,7 +439,7 @@ 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) } } 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..aa5c32ab9 --- /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.username = user.username + // 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..f49abd8ec --- /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 username: 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 + $username + .map { username in username.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/Segmented/Paging/ProfilePagingViewModel.swift b/Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift similarity index 56% rename from Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift rename to Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift index f8174acde..9b9e78d98 100644 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewModel.swift +++ b/Mastodon/Scene/Profile/Paging/ProfilePagingViewModel.swift @@ -18,6 +18,9 @@ final class ProfilePagingViewModel: NSObject { let mediaUserTimelineViewController = UserTimelineViewController() let profileAboutViewController = ProfileAboutViewController() + // input + @Published var needsSetupBottomShadow = true + init( postsUserTimelineViewModel: UserTimelineViewModel, repliesUserTimelineViewModel: UserTimelineViewModel, @@ -40,42 +43,8 @@ final class ProfilePagingViewModel: NSObject { ] } -// 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(.info, log: .debug, "%{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/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index ec43f4d4a..f572292ce 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -18,7 +18,7 @@ import TabBarPager import XLPagerTabStrip protocol ProfileViewModelEditable { - func isEdited() -> Bool + var isEdited: Bool { get } } final class ProfileViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -97,6 +97,8 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi private(set) lazy var profileHeaderViewController: ProfileHeaderViewController = { let viewController = ProfileHeaderViewController() + viewController.context = context + viewController.coordinator = coordinator viewController.viewModel = ProfileHeaderViewModel(context: context) return viewController }() @@ -121,30 +123,6 @@ final class ProfileViewController: UIViewController, NeedsDependency, MediaPrevi return profilePagingViewController }() -// 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 var profileBannerImageViewLayoutConstraint: NSLayoutConstraint! -// private(set) lazy var profileSegmentedViewController = ProfilePagingViewController() -// -// private var contentOffsets: [Int: CGFloat] = [:] -// var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation? - // title view nested in header var titleView: DoubleTitleLabelNavigationBarTitleView { profileHeaderViewController.titleView @@ -156,43 +134,17 @@ 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 var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + + profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets) + } override func viewDidLoad() { super.viewDidLoad() @@ -218,71 +170,71 @@ extension ProfileViewController { navigationItem.titleView = titleView -// let editingAndUpdatingPublisher = Publishers.CombineLatest( -// viewModel.$isEditing, -// viewModel.$isUpdating -// ) -// // note: not add .share() here -// -// let barButtonItemHiddenPublisher = Publishers.CombineLatest3( -// viewModel.isMeBarButtonItemsHidden.eraseToAnyPublisher(), -// viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(), -// viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher() -// ) -// -// editingAndUpdatingPublisher -// .receive(on: DispatchQueue.main) -// .sink { [weak self] isEditing, isUpdating in -// guard let self = self else { return } -// self.cancelEditingBarButtonItem.isEnabled = !isUpdating -// } -// .store(in: &disposeBag) + let editingAndUpdatingPublisher = Publishers.CombineLatest( + viewModel.$isEditing, + viewModel.$isUpdating + ) + // note: not add .share() here -// Publishers.CombineLatest4 ( -// viewModel.suspended.eraseToAnyPublisher(), -// profileHeaderViewController.viewModel.isTitleViewDisplaying.eraseToAnyPublisher(), -// editingAndUpdatingPublisher.eraseToAnyPublisher(), -// barButtonItemHiddenPublisher.eraseToAnyPublisher() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] suspended, 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 { -// 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) -// } -// if !isReplyBarButtonItemHidden { -// items.append(self.replyBarButtonItem) -// } -// } -// .store(in: &disposeBag) + let barButtonItemHiddenPublisher = Publishers.CombineLatest3( + viewModel.$isMeBarButtonItemsHidden, + viewModel.$isReplyBarButtonItemHidden, + viewModel.$isMoreMenuBarButtonItemHidden + ) + + editingAndUpdatingPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] isEditing, isUpdating in + guard let self = self else { return } + self.cancelEditingBarButtonItem.isEnabled = !isUpdating + } + .store(in: &disposeBag) + + Publishers.CombineLatest4 ( + viewModel.relationshipViewModel.$isSuspended, + profileHeaderViewController.viewModel.$isTitleViewDisplaying, + editingAndUpdatingPublisher.eraseToAnyPublisher(), + barButtonItemHiddenPublisher.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .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 !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) + } + if !isReplyBarButtonItemHidden { + items.append(self.replyBarButtonItem) + } + } + .store(in: &disposeBag) addChild(tabBarPagerController) tabBarPagerController.view.translatesAutoresizingMaskIntoConstraints = false @@ -300,156 +252,24 @@ extension ProfileViewController { tabBarPagerController.relayScrollView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(ProfileViewController.refreshControlValueChanged(_:)), for: .valueChanged) - -// overlayScrollView.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), -// ]) -// }) -// ) + + // setup delegate + profileHeaderViewController.delegate = self + profilePagingViewController.viewModel.profileAboutViewController.delegate = self + + bindViewModel() + bindTitleView() + bindMoreBarButtonItem() + bindPager() // 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 -// profileHeaderViewController.delegate = self -// profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.delegate = self -// profileSegmentedViewController.pagingViewController.pagingDelegate = self -// -// // bind view model -// bindProfile( -// headerViewModel: profileHeaderViewController.viewModel, -// aboutViewModel: profileAboutViewModel -// ) -// -// 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() } 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) @@ -479,256 +299,155 @@ extension ProfileViewController { } extension ProfileViewController { + + private func bindViewModel() { + // header + let headerViewModel = profileHeaderViewController.viewModel! + viewModel.$user + .assign(to: \.user, on: headerViewModel) + .store(in: &disposeBag) + viewModel.$isEditing + .assign(to: \.isEditing, on: headerViewModel) + .store(in: &disposeBag) + 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 bind(userTimelineViewModel: UserTimelineViewModel) { -// viewModel.domain -// 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 bindTitleView() { + Publishers.CombineLatest3( + profileHeaderViewController.profileHeaderView.viewModel.$name, + profileHeaderViewController.profileHeaderView.viewModel.$emojiMeta, + profileHeaderViewController.profileHeaderView.viewModel.$statusesCount + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] name, emojiMeta, statusesCount in + guard let self = self else { return } + guard let title = name, let statusesCount = statusesCount, + let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { + self.titleView.isHidden = true + return + } + self.titleView.isHidden = false + let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) + let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: mastodonContent) + self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) + } catch { + + } + } + .store(in: &disposeBag) + profileHeaderViewController.profileHeaderView.viewModel.$name + .receive(on: DispatchQueue.main) + .sink { [weak self] name in + guard let self = self else { return } + self.navigationItem.title = name + } + .store(in: &disposeBag) + } + + private func bindMoreBarButtonItem() { + Publishers.CombineLatest( + viewModel.$user, + viewModel.relationshipViewModel.$optionSet + ) + .asyncMap { [weak self] user, relationshipSet -> UIMenu? in + guard let self = self else { return nil } + guard let user = user else { + return nil + } + let name = user.displayNameWithFallback + let _ = ManagedObjectRecord(objectID: user.objectID) + let menu = MastodonMenu.setupMenu( + actions: [ + .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)), + ], + delegate: self + ) + return menu + } + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure: + self.moreMenuBarButtonItem.menu = nil + case .finished: + break + } + } receiveValue: { [weak self] menu in + guard let self = self else { return } + self.moreMenuBarButtonItem.menu = menu + } + .store(in: &disposeBag) + } + + private func bindPager() { + viewModel.$isPagingEnabled + .receive(on: DispatchQueue.main) + .sink { [weak self] isPagingEnabled in + guard let self = self else { return } + self.profilePagingViewController.containerView.isScrollEnabled = isPagingEnabled + self.profilePagingViewController.buttonBarView.isUserInteractionEnabled = isPagingEnabled + } + .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.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 bindProfile( -// headerViewModel: ProfileHeaderViewModel, -// aboutViewModel: ProfileAboutViewModel -// ) { -// // header -// viewModel.avatarImageURL -// .receive(on: DispatchQueue.main) -// .assign(to: \.avatarImageURL, on: headerViewModel.displayProfileInfo) -// .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 -// .assign(to: \.isEditing, on: headerViewModel) -// .store(in: &disposeBag) -// viewModel.isEditing -// .assign(to: \.isEditing, on: aboutViewModel) -// .store(in: &disposeBag) -// } -// -// private func bindTitleView() { -// Publishers.CombineLatest3( -// viewModel.name, -// viewModel.emojiMeta, -// viewModel.statusesCount -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak self] name, emojiMeta, statusesCount in -// guard let self = self else { return } -// guard let title = name, let statusesCount = statusesCount, -// let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { -// self.titleView.isHidden = true -// return -// } -// self.titleView.isHidden = false -// let subtitle = L10n.Plural.Count.MetricFormatted.post(formattedStatusCount, statusesCount) -// let mastodonContent = MastodonContent(content: title, emojis: emojiMeta) -// do { -// let metaContent = try MastodonMetaContent.convert(document: mastodonContent) -// self.titleView.update(titleMetaContent: metaContent, subtitle: subtitle) -// } catch { -// -// } -// } -// .store(in: &disposeBag) -// viewModel.name -// .receive(on: DispatchQueue.main) -// .sink { [weak self] name in -// guard let self = self else { return } -// self.navigationItem.title = name -// } -// .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() { -// Publishers.CombineLatest( -// viewModel.$user, -// viewModel.relationshipActionOptionSet -// ) -// .asyncMap { [weak self] user, relationshipSet -> UIMenu? in -// guard let self = self else { return nil } -// guard let user = user else { -// return nil -// } -// let name = user.displayNameWithFallback -// 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)), -// .reportUser(.init(name: name)), -// .shareUser(.init(name: name)), -// ], -// delegate: self -// ) -// return menu -// } -// .sink { [weak self] completion in -// guard let self = self else { return } -// switch completion { -// case .failure: -// self.moreMenuBarButtonItem.menu = nil -// case .finished: -// break -// } -// } receiveValue: { [weak self] menu in -// guard let self = self else { return } -// 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(), @@ -747,56 +466,23 @@ extension ProfileViewController { // } // .store(in: &disposeBag) // } // end func bindProfileRelationship -// -// private func bindProfileDashboard() { -// viewModel.statusesCount -// .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.postDashboardMeterView.numberLabel.text = text -// self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.isAccessibilityElement = true -// self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.accessibilityLabel = L10n.Plural.Count.post(count ?? 0) -// } -// .store(in: &disposeBag) -// viewModel.followingCount -// .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.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) -// } -// .store(in: &disposeBag) -// } -// -// private func handleMetaPress(_ meta: Meta) { -// switch meta { -// case .url(_, _, let url, _): -// guard let url = URL(string: url) else { return } -// coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) -// case .mention(_, _, let userInfo): -// guard let href = userInfo?["href"] as? String, -// let url = URL(string: href) else { return } -// coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) -// case .hashtag(_, let hashtag, _): -// let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) -// coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) -// case .email, .emoji: -// break -// } -// } + + private func handleMetaPress(_ meta: Meta) { + switch meta { + case .url(_, _, let url, _): + guard let url = URL(string: url) else { return } + coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + case .mention(_, _, let userInfo): + guard let href = userInfo?["href"] as? String, + let url = URL(string: href) else { return } + coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + case .hashtag(_, let hashtag, _): + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) + coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) + case .email, .emoji: + break + } + } } @@ -804,67 +490,66 @@ 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)) + 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 } -// let record: ManagedObjectRecord = .init(objectID: user.objectID) -// Task { -// let _activityViewController = try await DataSourceFacade.createActivityViewController( -// dependency: self, -// user: record -// ) -// guard let activityViewController = _activityViewController else { return } -// self.coordinator.present( -// scene: .activityViewController( -// activityViewController: activityViewController, -// sourceView: nil, -// barButtonItem: sender -// ), -// from: self, -// transition: .activityViewControllerPresent(animated: true, completion: nil) -// ) -// } // end Task + guard let user = viewModel.user else { return } + let record: ManagedObjectRecord = .init(objectID: user.objectID) + Task { + let _activityViewController = try await DataSourceFacade.createActivityViewController( + dependency: self, + user: record + ) + guard let activityViewController = _activityViewController else { return } + self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: nil, + barButtonItem: sender + ), + from: self, + transition: .activityViewControllerPresent(animated: true, completion: nil) + ) + } // 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) + 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 } -// guard let mastodonUser = viewModel.user else { return } -// let composeViewModel = ComposeViewModel( -// context: context, -// composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), -// authenticationBox: authenticationBox -// ) -// coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let mastodonUser = viewModel.user else { return } + let composeViewModel = ComposeViewModel( + context: context, + composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), + authenticationBox: authenticationBox + ) + 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 currentViewPage = profilePagingViewController.currentPage -// if let currentViewController = currentViewController as? UserTimelineViewController { -// currentViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) -// } -// -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { -// sender.endRefreshing() -// } + if let userTimelineViewController = profilePagingViewController.currentViewController as? UserTimelineViewController { + userTimelineViewController.viewModel.stateMachine.enter(UserTimelineViewModel.State.Reloading.self) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + sender.endRefreshing() + } } } @@ -883,14 +568,57 @@ extension ProfileViewController: TabBarPagerDelegate { } 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 - if scrollView.contentOffset.y < -scrollView.safeAreaInsets.top { - let offset = scrollView.contentOffset.y - (-scrollView.safeAreaInsets.top) - profileHeaderViewController.profileHeaderView.bannerImageViewTopLayoutConstraint.constant = offset - } else { - profileHeaderViewController.profileHeaderView.bannerImageViewTopLayoutConstraint.constant = 0 + + // 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 { + 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) } } + } // MARK: - TabBarPagerDataSource @@ -928,311 +656,219 @@ extension ProfileViewController: TabBarPagerDataSource { // } // // } -// -// // elastically banner image -// let headerScrollProgress = (containerScrollView.contentOffset.y - containerScrollView.safeAreaInsets.top) / topMaxContentOffsetY -// let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY -// profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle) // } // //} -// -//// 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, -// relationshipButtonDidPressed button: ProfileRelationshipActionButton -// ) { -// let relationshipActionSet = viewModel.relationshipActionOptionSet.value -// -// // 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 let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } -// guard let profileAboutViewModel = profileSegmentedViewController.pagingViewController.viewModel.profileAboutViewController.viewModel else { return } -// -// let isEdited = profileHeaderViewModel.isEdited() -// || profileAboutViewModel.isEdited() -// -// if isEdited { -// // update profile if changed -// viewModel.isUpdating.value = true -// Task { -// do { -// // TODO: handle error -// _ = try await viewModel.updateProfileInfo( -// headerProfileInfo: profileHeaderViewModel.editProfileInfo, -// aboutProfileInfo: profileAboutViewModel.editProfileInfo -// ) -// 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 -// -// } catch { -// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): update profile info fail: \(error.localizedDescription)") -// } -// -// // finish updating -// self.viewModel.isUpdating.value = false -// } -// } else { -// // set `updating` then toggle `edit` state -// viewModel.isUpdating.value = 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 -// } -// switch completion { -// case .failure(let error): -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s", ((#file as NSString).lastPathComponent), #line, #function, 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, handler: nil) -// alertController.addAction(okAction) -// self.coordinator.present( -// scene: .alertController(alertController: alertController), -// from: nil, -// transition: .alertController(animated: true, completion: nil) -// ) -// 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() -// } -// } receiveValue: { [weak self] response in -// guard let self = self else { return } -// self.viewModel.accountForEdit.value = response.value -// } -// .store(in: &disposeBag) -// } -// } else { -// guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } -// switch relationshipAction { -// case .none: -// break -// case .follow, .request, .pending, .following: -// guard let user = viewModel.user else { return } -// let reocrd = ManagedObjectRecord(objectID: user.objectID) -// guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// Task { -// try await DataSourceFacade.responseToUserFollowAction( -// dependency: self, -// user: reocrd, -// authenticationBox: authenticationBox -// ) -// } -// case .muting: -// guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// guard let user = viewModel.user else { return } -// let name = user.displayNameWithFallback -// -// let alertController = UIAlertController( -// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, -// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), -// preferredStyle: .alert -// ) -// let record = ManagedObjectRecord(objectID: user.objectID) -// let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in -// guard let self = self else { return } -// Task { -// try await DataSourceFacade.responseToUserMuteAction( -// dependency: self, -// user: record, -// authenticationBox: authenticationBox -// ) -// } -// } -// alertController.addAction(unmuteAction) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) -// alertController.addAction(cancelAction) -// present(alertController, animated: true, completion: nil) -// case .blocking: -// guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } -// guard let user = viewModel.user else { return } -// let name = user.displayNameWithFallback -// -// let alertController = UIAlertController( -// title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, -// message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), -// preferredStyle: .alert -// ) -// let record = ManagedObjectRecord(objectID: user.objectID) -// let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in -// guard let self = self else { return } -// Task { -// try await DataSourceFacade.responseToUserBlockAction( -// dependency: self, -// user: record, -// authenticationBox: authenticationBox -// ) -// } -// } -// alertController.addAction(unblockAction) -// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) -// alertController.addAction(cancelAction) -// present(alertController, animated: true, completion: nil) -// case .blocked: -// break -// default: -// assertionFailure() -// } -// } -// } -// -// func profileHeaderView(_ 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) { -// handleMetaPress(meta) -// } -//} -// -//// MARK: - MastodonMenuDelegate -//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, -// action: action, -// menuContext: DataSourceFacade.MenuContext( -// author: userRecord, -// status: nil, -// button: nil, -// barButtonItem: self.moreMenuBarButtonItem -// ), -// authenticationBox: authenticationBox -// ) -// } // end Task -// } -//} -// -//// MARK: - ScrollViewContainer -//extension ProfileViewController: ScrollViewContainer { -// var scrollView: UIScrollView? { -// return overlayScrollView -// } -//} -// + +// MARK: - ProfileHeaderViewControllerDelegate +extension ProfileViewController: ProfileHeaderViewControllerDelegate { + func profileHeaderViewController( + _ profileHeaderViewController: ProfileHeaderViewController, + profileHeaderView: ProfileHeaderView, + relationshipButtonDidPressed button: ProfileRelationshipActionButton + ) { + 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 else { return } + + guard let profileHeaderViewModel = profileHeaderViewController.viewModel else { return } + guard let profileAboutViewModel = profilePagingViewController.viewModel.profileAboutViewController.viewModel else { return } + + let isEdited = profileHeaderViewModel.isEdited || profileAboutViewModel.isEdited + + if isEdited { + // update profile when edited + viewModel.isUpdating = true + Task { @MainActor in + do { + // TODO: handle error + _ = try await viewModel.updateProfileInfo( + 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 = 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 = false + } // end Task + } else { + // set `updating` then toggle `edit` state + 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 = false + } + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch profile info for edit fail: %s", ((#file as NSString).lastPathComponent), #line, #function, 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, handler: nil) + alertController.addAction(okAction) + self.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + 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.toggle() + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + self.viewModel.accountForEdit = response.value + } + .store(in: &disposeBag) + } + } else { + guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return } + switch relationshipAction { + case .none: + break + case .follow, .request, .pending, .following: + guard let user = viewModel.user else { return } + let reocrd = ManagedObjectRecord(objectID: user.objectID) + guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return } + Task { + try await DataSourceFacade.responseToUserFollowAction( + dependency: self, + user: reocrd, + authenticationBox: authenticationBox + ) + } + case .muting: + guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let user = viewModel.user else { return } + let name = user.displayNameWithFallback + + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name), + preferredStyle: .alert + ) + let record = ManagedObjectRecord(objectID: user.objectID) + let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unmute, style: .default) { [weak self] _ in + guard let self = self else { return } + Task { + try await DataSourceFacade.responseToUserMuteAction( + dependency: self, + user: record, + authenticationBox: authenticationBox + ) + } + } + alertController.addAction(unmuteAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + case .blocking: + guard let authenticationBox = self.context.authenticationService.activeMastodonAuthenticationBox.value else { return } + guard let user = viewModel.user else { return } + let name = user.displayNameWithFallback + + let alertController = UIAlertController( + title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.title, + message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUser.message(name), + preferredStyle: .alert + ) + let record = ManagedObjectRecord(objectID: user.objectID) + let unblockAction = UIAlertAction(title: L10n.Common.Controls.Friendship.unblock, style: .default) { [weak self] _ in + guard let self = self else { return } + Task { + try await DataSourceFacade.responseToUserBlockAction( + dependency: self, + user: record, + authenticationBox: authenticationBox + ) + } + } + alertController.addAction(unblockAction) + let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + case .blocked: + break + default: + assertionFailure() + } + } + + } + + func profileHeaderViewController( + _ profileHeaderViewController: ProfileHeaderViewController, + profileHeaderView: ProfileHeaderView, + metaTextView: MetaTextView, + metaDidPressed meta: Meta + ) { + handleMetaPress(meta) + } +} + +// MARK: - ProfileAboutViewControllerDelegate +extension ProfileViewController: ProfileAboutViewControllerDelegate { + func profileAboutViewController( + _ viewController: ProfileAboutViewController, + profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, + metaLabel: MetaLabel, + didSelectMeta meta: Meta + ) { + handleMetaPress(meta) + } +} + +// MARK: - MastodonMenuDelegate +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, + action: action, + menuContext: DataSourceFacade.MenuContext( + author: userRecord, + status: nil, + button: nil, + barButtonItem: self.moreMenuBarButtonItem + ), + authenticationBox: authenticationBox + ) + } // end Task + } +} + +// MARK: - ScrollViewContainer +extension ProfileViewController: ScrollViewContainer { + var scrollView: UIScrollView { + return tabBarPagerController.containerScrollView + } +} + //extension ProfileViewController { // // override var keyCommands: [UIKeyCommand]? { diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index add87f312..91866b851 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -41,74 +41,38 @@ class ProfileViewModel: NSObject { @Published var isEditing = false @Published var isUpdating = false + @Published var accountForEdit: Mastodon.Entity.Account? // output + let relationshipViewModel = RelationshipViewModel() + @Published var userIdentifier: UserIdentifier? = nil + + @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 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 relationshipActionOptionSet = CurrentValueSubject(.none) -// let isFollowedBy = CurrentValueSubject(false) -// let isMuting = CurrentValueSubject(false) -// let isBlocking = CurrentValueSubject(false) -// let isBlockedBy = CurrentValueSubject(false) -// -// let isRelationshipActionButtonHidden = CurrentValueSubject(true) -// let isReplyBarButtonItemHidden = CurrentValueSubject(true) -// let isMoreMenuBarButtonItemHidden = CurrentValueSubject(true) -// let isMeBarButtonItemsHidden = CurrentValueSubject(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) @@ -122,6 +86,9 @@ class ProfileViewModel: NSObject { self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user } .store(in: &disposeBag) + $me + .assign(to: \.me, on: relationshipViewModel) + .store(in: &disposeBag) // bind user $user @@ -130,250 +97,88 @@ class ProfileViewModel: NSObject { 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) - // $userIdentifier.assign(to: &profileAboutViewModel.$userIdentifier) -// relationshipActionOptionSet -// .compactMap { $0.highPriorityAction(except: []) } -// .map { $0 == .none } -// .assign(to: \.value, on: isRelationshipActionButtonHidden) -// .store(in: &disposeBag) -// + // 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, -// context.authenticationService.activeMastodonAuthenticationBox, -// pendingRetryPublisher -// ) -// .sink { [weak self] userRecord, authenticationBox, _ in -// guard let self = self else { return } -// guard let userRecord = userRecord, -// let authenticationBox = authenticationBox -// else { return } -// Task { -// do { -// let response = try await self.updateRelationship( -// record: userRecord, -// authenticationBox: authenticationBox -// ) -// // there are seconds delay after request follow before requested -> following. Query again when needs -// guard let relationship = response.value.first else { return } -// if relationship.requested == true { -// let delay = pendingRetryPublisher.value -// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in -// guard let _ = self else { return } -// pendingRetryPublisher.value = min(2 * delay, 60) -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function) -// } -// } -// } 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)") -// } -// } -// } -// .store(in: &disposeBag) -// -// let isBlockingOrBlocked = Publishers.CombineLatest( -// isBlocking, -// isBlockedBy -// ) -// .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() - } - -} + // query relationship + let userRecord = $user.map { user -> ManagedObjectRecord? in + user.flatMap { ManagedObjectRecord(objectID: $0.objectID) } + } + let pendingRetryPublisher = CurrentValueSubject(1) -extension ProfileViewModel { - private func setup() { - Publishers.CombineLatest( - $user, - $me + // observe friendship + Publishers.CombineLatest3( + userRecord, + context.authenticationService.activeMastodonAuthenticationBox, + pendingRetryPublisher ) - .receive(on: DispatchQueue.main) - .sink { [weak self] user, me in + .sink { [weak self] userRecord, authenticationBox, _ 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 + guard let userRecord = userRecord, + let authenticationBox = authenticationBox + else { return } + Task { + do { + let response = try await self.updateRelationship( + record: userRecord, + authenticationBox: authenticationBox + ) + // there are seconds delay after request follow before requested -> following. Query again when needs + guard let relationship = response.value.first else { return } + if relationship.requested == true { + let delay = pendingRetryPublisher.value + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let _ = self else { return } + pendingRetryPublisher.value = min(2 * delay, 60) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function) } } - - } 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 - } + } 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( + relationshipViewModel.$isBlocking, + relationshipViewModel.$isBlockingBy + ) + .map { $0 || $1 } + .share() + + Publishers.CombineLatest( + isBlockingOrBlocked, + $isEditing + ) + .map { !$0 && !$1 } + .assign(to: &$isPagingEnabled) } - 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 -// } - } - } extension ProfileViewModel { @@ -418,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 4eca4268e..000000000 --- a/Mastodon/Scene/Profile/Segmented/Paging/ProfilePagingViewController.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ProfilePagingViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-29. -// - -import os.log -import UIKit -import XLPagerTabStrip -import TabBarPager - -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 viewModel: ProfilePagingViewModel! - - // 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() { - 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 currentPage?.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/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 54f936f75..fb42b81b8 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewController.swift @@ -178,6 +178,6 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable { // MARK: - IndicatorInfoProvider extension UserTimelineViewController: IndicatorInfoProvider { func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { - return IndicatorInfo(title: "Hello") + return IndicatorInfo(title: viewModel.title) } } diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift index 0fc3368d1..7f7341aa6 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+Diffable.swift @@ -40,9 +40,9 @@ extension UserTimelineViewModel { .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 71213489f..ca798fa0b 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -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 8882990ea..2d350fb0b 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel.swift @@ -19,16 +19,18 @@ final class UserTimelineViewModel { // input let context: AppContext - @Published var userIdentifier: UserIdentifier? - @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? @@ -47,9 +49,11 @@ final class UserTimelineViewModel { init( context: AppContext, + title: String, queryFilter: QueryFilter ) { self.context = context + self.title = title self.statusFetchedResultsController = StatusFetchedResultsController( managedObjectContext: context.managedObjectContext, domain: nil, 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/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 } From fceee88ea6528fea37449eb6731b6276ab7f0a3a Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 27 May 2022 00:00:27 +0800 Subject: [PATCH 15/27] fix: notification includes admin mod type issue ref: https://github.com/mastodon/mastodon/pull/17767 --- ...ionTimelineViewModel+LoadOldestState.swift | 2 +- .../NotificationTimelineViewModel.swift | 1 - .../APIService/APIService+Notification.swift | 22 +++++++++++++++++++ .../API/Mastodon+API+Notifications.swift | 8 +++++++ 4 files changed, 31 insertions(+), 2 deletions(-) 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/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/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)) From 15be4a9ce3766c4da35ab2c0052c3d84d0d6a6bb Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 27 May 2022 00:01:31 +0800 Subject: [PATCH 16/27] fix: account notification badge color only set to Dark Mode style issue --- .../Account/Cell/AccountListTableViewCell.swift | 1 - Mastodon/Scene/Account/View/BadgeButton.swift | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) 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) From 5467a04a6dc9722d6b18782bfc19106acccab55f Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 27 May 2022 00:10:48 +0800 Subject: [PATCH 17/27] chore: update version to 1.4.3 (134) --- AppShared/Info.plist | 4 +-- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++++++++--------------- Mastodon/Info.plist | 4 +-- MastodonIntent/Info.plist | 4 +-- MastodonTests/Info.plist | 4 +-- MastodonUITests/Info.plist | 4 +-- NotificationService/Info.plist | 4 +-- ShareActionExtension/Info.plist | 4 +-- 8 files changed, 38 insertions(+), 38 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index bc0be73ac..c3b8e9ee7 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 + 134 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b3ab9234b..3e10f428a 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = 134; 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 = 134; 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 = 134; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 133; + DYLIB_CURRENT_VERSION = 134; 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 = 134; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 133; + DYLIB_CURRENT_VERSION = 134; 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 = 134; 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 = 134; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 133; + DYLIB_CURRENT_VERSION = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 133; + DYLIB_CURRENT_VERSION = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; 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 = 134; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 66fdfcaaf..c13c66aa2 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 + 134 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 1a6ccd5fb..590139d62 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 + 134 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index bc0be73ac..c3b8e9ee7 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 + 134 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index bc0be73ac..c3b8e9ee7 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 + 134 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 31a95bf2b..2377790fc 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 + 134 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 7df1564a3..a15df3198 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 + 134 NSExtension NSExtensionAttributes From 8dc97bf754abed415e2850f784bbad1e7eceafe6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 31 May 2022 20:39:18 +0200 Subject: [PATCH 18/27] New translations app.json (Kabyle) --- Localization/StringsConvertor/input/kab.lproj/app.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 8987669c0..2b0aa748f 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" } } From d1f87c90219234aeecbb7b607e2e72da77253ddd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 31 May 2022 21:38:16 +0200 Subject: [PATCH 19/27] New translations app.json (Kabyle) --- .../StringsConvertor/input/kab.lproj/app.json | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/Localization/StringsConvertor/input/kab.lproj/app.json b/Localization/StringsConvertor/input/kab.lproj/app.json index 2b0aa748f..fa2cac641 100644 --- a/Localization/StringsConvertor/input/kab.lproj/app.json +++ b/Localization/StringsConvertor/input/kab.lproj/app.json @@ -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": { From daf96915d2de4ac6a2b562140f67958fea5f2265 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Jun 2022 17:52:03 +0800 Subject: [PATCH 20/27] chore: update i18n resources --- .../Resources/fr.lproj/Localizable.strings | 8 +- .../Resources/gl.lproj/Localizable.strings | 14 ++-- .../Resources/it.lproj/Localizable.strings | 8 +- .../Resources/ja.lproj/Localizable.strings | 50 ++++++------ .../Resources/kab.lproj/Localizable.strings | 76 +++++++++---------- .../Resources/ku.lproj/Localizable.strings | 2 +- .../Resources/th.lproj/Localizable.strings | 2 +- .../Resources/vi.lproj/Localizable.strings | 2 +- 8 files changed, 81 insertions(+), 81 deletions(-) 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"; From 5394ba93a4a8e8c10c8b3dd00d191c6a1d1ee7e5 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Jun 2022 18:37:24 +0800 Subject: [PATCH 21/27] fix: paragraph spacing may lost issue --- MastodonSDK/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"), From a19293a1633613b325dea7feb06312cd6ed1adc4 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Jun 2022 18:37:51 +0800 Subject: [PATCH 22/27] chore: update version to 1.4.3 (135) --- AppShared/Info.plist | 2 +- Mastodon.xcodeproj/project.pbxproj | 65 +++++++------------ .../xcschemes/xcschememanagement.plist | 4 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- ShareActionExtension/Info.plist | 2 +- 10 files changed, 35 insertions(+), 52 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index c3b8e9ee7..800749233 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3e10f428a..ab62856fa 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -234,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 */; }; @@ -1473,7 +1472,6 @@ EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, DB3EA904281BBD9400598866 /* Introspect in Frameworks */, DB3EA902281BBD5D00598866 /* CommonOSLog in Frameworks */, - DB3EA900281BBB1D00598866 /* MetaTextKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3510,7 +3508,6 @@ DB3EA8F4281BB65200598866 /* MastodonSDK */, DB3EA8FB281BBAE100598866 /* AlamofireImage */, DB3EA8FD281BBAF200598866 /* Alamofire */, - DB3EA8FF281BBB1D00598866 /* MetaTextKit */, DB3EA901281BBD5D00598866 /* CommonOSLog */, DB3EA903281BBD9400598866 /* Introspect */, ); @@ -3663,7 +3660,6 @@ 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" */, @@ -4839,7 +4835,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4869,7 +4865,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -4977,11 +4973,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 134; + DYLIB_CURRENT_VERSION = 135; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5008,11 +5004,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 134; + DYLIB_CURRENT_VERSION = 135; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5103,7 +5099,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5171,11 +5167,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 134; + DYLIB_CURRENT_VERSION = 135; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5200,7 +5196,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5223,7 +5219,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5247,7 +5243,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5271,7 +5267,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5295,7 +5291,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5319,7 +5315,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5343,7 +5339,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5430,7 +5426,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; @@ -5497,11 +5493,11 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 134; + DYLIB_CURRENT_VERSION = 135; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AppShared/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -5525,7 +5521,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5548,7 +5544,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = ShareActionExtension/ShareActionExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = ShareActionExtension/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5572,7 +5568,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = MastodonIntent/MastodonIntent.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonIntent/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5596,7 +5592,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5619,7 +5615,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -5763,14 +5759,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"; @@ -5912,11 +5900,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" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 6817fbe8e..c898c8850 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 23 + 24 MastodonIntents.xcscheme_^#shared#^_ @@ -134,7 +134,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 23 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 517917535..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" } }, { diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index c13c66aa2..47b8a8a14 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 134 + 135 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 590139d62..f5f88031a 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index c3b8e9ee7..800749233 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index c3b8e9ee7..800749233 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 2377790fc..0c18e0fe4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index a15df3198..b8d026bb1 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 134 + 135 NSExtension NSExtensionAttributes From f49be82d2b54fdf910d248edf83c2a7d3d3be8ea Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Jun 2022 19:33:10 +0800 Subject: [PATCH 23/27] chore: restore profile keyboard navigation --- Mastodon.xcodeproj/project.pbxproj | 4 + .../xcschemes/xcschememanagement.plist | 16 +-- .../Protocol/PagerTabStripNavigateable.swift | 106 ++++++++++++++++++ ...tatusTableViewControllerNavigateable.swift | 6 + .../Scene/Profile/ProfileViewController.swift | 75 ++++--------- 5 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 Mastodon/Protocol/PagerTabStripNavigateable.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ab62856fa..f232aab45 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -378,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 */; }; @@ -1139,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 = ""; }; @@ -1750,6 +1752,7 @@ DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */, DB1D843326579931000346B3 /* TableViewControllerNavigateable.swift */, DB1D842D26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift */, + DB6988DD2848D11C002398EF /* PagerTabStripNavigateable.swift */, ); path = Protocol; sourceTree = ""; @@ -4250,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 */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index c898c8850..b10d1159a 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 + 8 CoreDataStack.xcscheme_^#shared#^_ @@ -24,22 +24,22 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 7 + 10 Mastodon - Release.xcscheme_^#shared#^_ orderHint - 2 + 4 Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 3 + 5 Mastodon - ar.xcscheme orderHint - 4 + 7 Mastodon - ar.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 24 + 42 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 22 + 43 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 23 + 41 SuppressBuildableAutocreation 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/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index f572292ce..6a51ff599 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -261,7 +261,6 @@ extension ProfileViewController { bindTitleView() bindMoreBarButtonItem() bindPager() -// updateBarButtonInsets() } override func viewDidAppear(_ animated: Bool) { @@ -270,32 +269,6 @@ extension ProfileViewController { setNeedsStatusBarAppearanceUpdate() } - 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 { @@ -869,28 +842,28 @@ extension ProfileViewController: ScrollViewContainer { } } -//extension ProfileViewController { -// -// override var keyCommands: [UIKeyCommand]? { -// if !viewModel.isEditing.value { -// return pageboyNavigateKeyCommands -// } -// -// return nil -// } -// -//} -// -//// MARK: - PageboyNavigateable -//extension ProfileViewController: PageboyNavigateable { -// -// var navigateablePageViewController: PageboyViewController { -// return profileSegmentedViewController.pagingViewController -// } -// -// @objc func pageboyNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { -// pageboyNavigateKeyCommandHandler(sender) -// } -// -//} +extension ProfileViewController { + + override var keyCommands: [UIKeyCommand]? { + if !viewModel.isEditing { + return pagerTabStripNavigateKeyCommands + } + + return nil + } + +} + +// MARK: - PagerTabStripNavigateable +extension ProfileViewController: PagerTabStripNavigateable { + + var navigateablePageViewController: PagerTabStripViewController { + return profilePagingViewController + } + + @objc func pagerTabStripNavigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { + pagerTabStripNavigateKeyCommandHandler(sender) + } + +} From 229d75b76ab82b2a6f7e766a183b6c6d60a43bf7 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 2 Jun 2022 19:33:29 +0800 Subject: [PATCH 24/27] chore: update version to 1.4.3 (136) --- AppShared/Info.plist | 2 +- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++---------- .../xcschemes/xcschememanagement.plist | 6 +-- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- ShareActionExtension/Info.plist | 2 +- 9 files changed, 34 insertions(+), 34 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index 800749233..a2908c7c9 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f232aab45..f0e811f29 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4839,7 +4839,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 135; + DYLIB_CURRENT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 135; + DYLIB_CURRENT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 135; + DYLIB_CURRENT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 135; + DYLIB_CURRENT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; 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 = 135; + CURRENT_PROJECT_VERSION = 136; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index b10d1159a..29b65ef86 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 42 + 44 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 43 + 45 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 41 + 46 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 47b8a8a14..710210665 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 135 + 136 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index f5f88031a..149eb452e 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index 800749233..a2908c7c9 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index 800749233..a2908c7c9 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 0c18e0fe4..11f26161f 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index b8d026bb1..07b2e3466 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 135 + 136 NSExtension NSExtensionAttributes From b91b3fa23d8ed5dcf40c9dc35bb26b620c794ab0 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 5 Jun 2022 16:41:47 +0800 Subject: [PATCH 25/27] fix: use acct instead of username in profile header view --- .../Header/View/ProfileHeaderView+Configuration.swift | 2 +- .../Profile/Header/View/ProfileHeaderView+ViewModel.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift index aa5c32ab9..ac33227cc 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+Configuration.swift @@ -33,7 +33,7 @@ extension ProfileHeaderView { .assign(to: \.name, on: viewModel) .store(in: &disposeBag) // username - viewModel.username = user.username + viewModel.acct = user.acctWithDomain // bio user.publisher(for: \.note) .assign(to: \.note, on: viewModel) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift index f49abd8ec..808e1d7ba 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView+ViewModel.swift @@ -33,7 +33,7 @@ extension ProfileHeaderView { @Published var name: String? @Published var nameEditing: String? - @Published var username: String? + @Published var acct: String? @Published var note: String? @Published var noteEditing: String? @@ -133,8 +133,8 @@ extension ProfileHeaderView.ViewModel { } .store(in: &disposeBag) // username - $username - .map { username in username.flatMap { "@" + $0 } ?? " " } + $acct + .map { acct in acct.flatMap { "@" + $0 } ?? " " } .assign(to: \.text, on: view.usernameLabel) .store(in: &disposeBag) // bio From c8fa5430d38869e2125b3092a03c05f201ad31af Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 5 Jun 2022 16:42:14 +0800 Subject: [PATCH 26/27] fix: checkmark background color lost for unselected poll issue --- .../Share/View/Content/PollOptionView+Configuration.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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) } From ed2e1bb46231e8dbf615cb7f7529debb3c6f3f35 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 5 Jun 2022 16:43:04 +0800 Subject: [PATCH 27/27] chore: update version to 1.4.3 (138) --- AppShared/Info.plist | 2 +- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++---------- .../xcschemes/xcschememanagement.plist | 14 +++--- Mastodon/Info.plist | 2 +- MastodonIntent/Info.plist | 2 +- MastodonTests/Info.plist | 2 +- MastodonUITests/Info.plist | 2 +- NotificationService/Info.plist | 2 +- ShareActionExtension/Info.plist | 2 +- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/AppShared/Info.plist b/AppShared/Info.plist index a2908c7c9..094d6d538 100644 --- a/AppShared/Info.plist +++ b/AppShared/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f0e811f29..efe519f74 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -4839,7 +4839,7 @@ CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 136; + 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 = 136; + 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 = 136; + CURRENT_PROJECT_VERSION = 138; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 136; + 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 = 136; + CURRENT_PROJECT_VERSION = 138; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 136; + 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 = 136; + 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 = 136; + CURRENT_PROJECT_VERSION = 138; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + CURRENT_PROJECT_VERSION = 138; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + 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 = 136; + CURRENT_PROJECT_VERSION = 138; DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 29b65ef86..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 - 8 + 9 CoreDataStack.xcscheme_^#shared#^_ @@ -24,7 +24,7 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 10 + 11 Mastodon - Release.xcscheme_^#shared#^_ @@ -34,12 +34,12 @@ Mastodon - Snapshot.xcscheme_^#shared#^_ orderHint - 5 + 6 Mastodon - ar.xcscheme orderHint - 7 + 8 Mastodon - ar.xcscheme_^#shared#^_ @@ -114,7 +114,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 44 + 30 MastodonIntents.xcscheme_^#shared#^_ @@ -129,12 +129,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 45 + 29 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 46 + 31 SuppressBuildableAutocreation diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index 710210665..0407f6502 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -43,7 +43,7 @@ CFBundleVersion - 136 + 138 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/MastodonIntent/Info.plist b/MastodonIntent/Info.plist index 149eb452e..2f2b230f0 100644 --- a/MastodonIntent/Info.plist +++ b/MastodonIntent/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 NSExtension NSExtensionAttributes diff --git a/MastodonTests/Info.plist b/MastodonTests/Info.plist index a2908c7c9..094d6d538 100644 --- a/MastodonTests/Info.plist +++ b/MastodonTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 diff --git a/MastodonUITests/Info.plist b/MastodonUITests/Info.plist index a2908c7c9..094d6d538 100644 --- a/MastodonUITests/Info.plist +++ b/MastodonUITests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist index 11f26161f..ffeca43f4 100644 --- a/NotificationService/Info.plist +++ b/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 NSExtension NSExtensionPointIdentifier diff --git a/ShareActionExtension/Info.plist b/ShareActionExtension/Info.plist index 07b2e3466..77557d6b7 100644 --- a/ShareActionExtension/Info.plist +++ b/ShareActionExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4.3 CFBundleVersion - 136 + 138 NSExtension NSExtensionAttributes