Merge branch 'develop' into performance_experiments

This commit is contained in:
Marcus Kida 2023-09-29 12:43:34 +02:00
commit 943f3bc778
No known key found for this signature in database
GPG Key ID: 19FF64E08013CA40
162 changed files with 2789 additions and 4314 deletions

View File

@ -709,59 +709,60 @@
"title": "Post from %s"
},
"settings": {
"title": "Settings",
"section": {
"overview": {
"title": "Settings",
"general": "General",
"notifications": "Notifications",
"support_mastodon": "Support Mastodon",
"about_mastodon": "About Mastodon",
"logout": "Logout %@"
}
"about_mastodon": {
"title": "About",
"more_settings": "Even More Settings",
"contribute_to_mastodon": "Contribute to Mastodon",
"privacy_policy": "Privacy Policy",
"clear_media_storage": "Clear Media Storage"
},
"general": {
"title": "General",
"appearance": {
"title": "Appearance",
"automatic": "Automatic",
"light": "Always Light",
"dark": "Always Dark"
"section_title": "Appearance",
"dark": "Dark",
"light": "Light",
"system": "Use Device Appearance"
},
"look_and_feel": {
"title": "Look and Feel",
"use_system": "Use System",
"really_dark": "Really Dark",
"sorta_dark": "Sorta Dark",
"light": "Light"
"design": {
"section_title": "Design",
"show_animations": "Play Animated Avatars and Emoji"
},
"notifications": {
"title": "Notifications",
"favorites": "Favorites my post",
"follows": "Follows me",
"boosts": "Reblogs my post",
"mentions": "Mentions me",
"trigger": {
"anyone": "anyone",
"follower": "a follower",
"follow": "anyone I follow",
"noone": "no one",
"title": "Notify me when"
}
},
"preference": {
"title": "Preferences",
"disable_avatar_animation": "Disable animated avatars",
"disable_emoji_animation": "Disable animated emojis",
"using_default_browser": "Use default browser to open links",
"open_links_in_mastodon": "Open links in Mastodon"
},
"boring_zone": {
"title": "The Boring Zone",
"account_settings": "Account Settings",
"terms": "Terms of Service",
"privacy": "Privacy Policy"
},
"spicy_zone": {
"title": "The Spicy Zone",
"clear": "Clear Media Cache",
"signout": "Sign Out"
"links": {
"section_title": "Links",
"open_in_mastodon": "Open in Mastodon",
"open_in_browser": "Open in Browser"
}
},
"footer": {
"mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
},
"keyboard": {
"close_settings_window": "Close Settings Window"
"notifications": {
"title": "Notifications",
"policy": {
"title": "Get Notifications from",
"anyone": "Anyone",
"followers": "People who follow you",
"follow": "People you follow",
"noone": "No one"
},
"alert": {
"mentions_and_replies": "Mentions & Replies",
"boosts": "Boosts",
"favorites": "Favorites",
"new_followers": "New Followers"
},
"disabled": {
"notification_hint": "Turn on notifications from your device settings to see updates on your lock screen.",
"go_to_settings": "Go to Notification Settings"
}
}
},
"report": {

View File

@ -655,6 +655,10 @@
"profile": "Go to @%s@%s",
"url": "Open URL in Mastodon",
"hashtag": "Go to #%s",
"no_user": {
"title": "No User Account Found",
"message": "There's no Useraccount \"%s\" on %s"
}
"empty_state": {
"no_results": "No results"
},
@ -705,59 +709,60 @@
"title": "Post from %s"
},
"settings": {
"title": "Settings",
"section": {
"overview": {
"title": "Settings",
"general": "General",
"notifications": "Notifications",
"support_mastodon": "Support Mastodon",
"about_mastodon": "About Mastodon",
"logout": "Logout %@"
}
"about_mastodon": {
"title": "About",
"more_settings": "Even More Settings",
"contribute_to_mastodon": "Contribute to Mastodon",
"privacy_policy": "Privacy Policy",
"clear_media_storage": "Clear Media Storage"
},
"general": {
"title": "General",
"appearance": {
"title": "Appearance",
"automatic": "Automatic",
"light": "Always Light",
"dark": "Always Dark"
"section_title": "Appearance",
"dark": "Dark",
"light": "Light",
"system": "Use Device Appearance"
},
"look_and_feel": {
"title": "Look and Feel",
"use_system": "Use System",
"really_dark": "Really Dark",
"sorta_dark": "Sorta Dark",
"light": "Light"
"design": {
"section_title": "Design",
"show_animations": "Play Animated Avatars and Emoji"
},
"notifications": {
"title": "Notifications",
"favorites": "Favorites my post",
"follows": "Follows me",
"boosts": "Reblogs my post",
"mentions": "Mentions me",
"trigger": {
"anyone": "anyone",
"follower": "a follower",
"follow": "anyone I follow",
"noone": "no one",
"title": "Notify me when"
}
},
"preference": {
"title": "Preferences",
"disable_avatar_animation": "Disable animated avatars",
"disable_emoji_animation": "Disable animated emojis",
"using_default_browser": "Use default browser to open links",
"open_links_in_mastodon": "Open links in Mastodon"
},
"boring_zone": {
"title": "The Boring Zone",
"account_settings": "Account Settings",
"terms": "Terms of Service",
"privacy": "Privacy Policy"
},
"spicy_zone": {
"title": "The Spicy Zone",
"clear": "Clear Media Cache",
"signout": "Sign Out"
"links": {
"section_title": "Links",
"open_in_mastodon": "Open in Mastodon",
"open_in_browser": "Open in Browser"
}
},
"footer": {
"mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
},
"keyboard": {
"close_settings_window": "Close Settings Window"
"notifications": {
"title": "Notifications",
"policy": {
"title": "Get Notifications from",
"anyone": "Anyone",
"followers": "People who follow you",
"follow": "People you follow",
"noone": "No one"
},
"alert": {
"mentions_and_replies": "Mentions & Replies",
"boosts": "Boosts",
"favorites": "Favorites",
"new_followers": "New Followers"
},
"disabled": {
"notification_hint": "Turn on notifications from your device settings to see updates on your lock screen.",
"go_to_settings": "Go to Notification Settings"
}
}
},
"report": {

View File

@ -709,59 +709,60 @@
"title": "Post from %s"
},
"settings": {
"title": "Settings",
"section": {
"overview": {
"title": "Settings",
"general": "General",
"notifications": "Notifications",
"support_mastodon": "Support Mastodon",
"about_mastodon": "About Mastodon",
"logout": "Logout %@"
}
"about_mastodon": {
"title": "About",
"more_settings": "Even More Settings",
"contribute_to_mastodon": "Contribute to Mastodon",
"privacy_policy": "Privacy Policy",
"clear_media_storage": "Clear Media Storage"
},
"general": {
"title": "General",
"appearance": {
"title": "Appearance",
"automatic": "Automatic",
"light": "Always Light",
"dark": "Always Dark"
"section_title": "Appearance",
"dark": "Dark",
"light": "Light",
"system": "Use Device Appearance"
},
"look_and_feel": {
"title": "Look and Feel",
"use_system": "Use System",
"really_dark": "Really Dark",
"sorta_dark": "Sorta Dark",
"light": "Light"
"design": {
"section_title": "Design",
"show_animations": "Play Animated Avatars and Emoji"
},
"notifications": {
"title": "Notifications",
"favorites": "Favorites my post",
"follows": "Follows me",
"boosts": "Reblogs my post",
"mentions": "Mentions me",
"trigger": {
"anyone": "anyone",
"follower": "a follower",
"follow": "anyone I follow",
"noone": "no one",
"title": "Notify me when"
}
},
"preference": {
"title": "Preferences",
"disable_avatar_animation": "Disable animated avatars",
"disable_emoji_animation": "Disable animated emojis",
"using_default_browser": "Use default browser to open links",
"open_links_in_mastodon": "Open links in Mastodon"
},
"boring_zone": {
"title": "The Boring Zone",
"account_settings": "Account Settings",
"terms": "Terms of Service",
"privacy": "Privacy Policy"
},
"spicy_zone": {
"title": "The Spicy Zone",
"clear": "Clear Media Cache",
"signout": "Sign Out"
"links": {
"section_title": "Links",
"open_in_mastodon": "Open in Mastodon",
"open_in_browser": "Open in Browser"
}
},
"footer": {
"mastodon_description": "Mastodon is open source software. You can report issues on GitHub at %s (%s)"
},
"keyboard": {
"close_settings_window": "Close Settings Window"
"notifications": {
"title": "Notifications",
"policy": {
"title": "Get Notifications from",
"anyone": "Anyone",
"followers": "People who follow you",
"follow": "People you follow",
"noone": "No one"
},
"alert": {
"mentions_and_replies": "Mentions & Replies",
"boosts": "Boosts",
"favorites": "Favorites",
"new_followers": "New Followers"
},
"disabled": {
"notification_hint": "Turn on notifications from your device settings to see updates on your lock screen.",
"go_to_settings": "Go to Notification Settings"
}
}
},
"report": {

View File

@ -103,11 +103,6 @@
357FEEBA29523D660021C9DC /* MastodonSDKDynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 357FEEB929523D660021C9DC /* MastodonSDKDynamic */; };
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */; };
5B24BBDB262DB14800A9381B /* ReportStatusViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */; };
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; };
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; };
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */; };
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45C262599800002E742 /* SettingsSectionHeader.swift */; };
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */; };
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; };
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
@ -140,6 +135,14 @@
D81A22752AB4643200905D71 /* SearchResultsOverviewTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */; };
D81A22782AB4782400905D71 /* SearchResultOverviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */; };
D81A227B2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */; };
D81D12462A4E1861005009D4 /* PolicySelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81D12452A4E1861005009D4 /* PolicySelectionViewController.swift */; };
D81D124B2A4E1914005009D4 /* ToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81D124A2A4E1914005009D4 /* ToggleTableViewCell.swift */; };
D82BD7532ABC44C2009A374A /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F9170E2A4B47EF008A5370 /* Coordinator.swift */; };
D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82BD7542ABC73AF009A374A /* NotificationPolicyTableViewCell.swift */; };
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A7F2A4466D300C0FB73 /* SettingsCoordinator.swift */; };
D8318A862A4468C700C0FB73 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A852A4468C700C0FB73 /* SettingsViewController.swift */; };
D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */; };
D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A892A4468DC00C0FB73 /* AboutViewController.swift */; };
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8363B1529469CE200A74079 /* OnboardingNextView.swift */; };
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
@ -147,15 +150,29 @@
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D886FBD229DF710F00272017 /* WelcomeSeparatorView.swift */; };
D8916DC029211BE500124085 /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8916DBF29211BE500124085 /* ContentSizedTableView.swift */; };
D8A6AB6C291C5136003AB663 /* MastodonLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A6AB6B291C5136003AB663 /* MastodonLoginViewController.swift */; };
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */; };
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */; };
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */; };
D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; };
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */; };
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; };
D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; };
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */; };
D8ECC8102AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */; };
D8F0372C29D232730027DE2E /* HashtagIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */; };
D8F8A03A29CA5C15000195DD /* HashtagWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */; };
D8F8A03C29CA5CB6000195DD /* HashtagWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F8A03B29CA5CB6000195DD /* HashtagWidget.swift */; };
D8F917012A4AD8A5008A5370 /* SettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917002A4AD8A4008A5370 /* SettingsTableViewCell.swift */; };
D8F917032A4B063D008A5370 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917022A4B063D008A5370 /* Settings.swift */; };
D8F917062A4B0791008A5370 /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917052A4B0791008A5370 /* GeneralSettings.swift */; };
D8F917082A4B0B16008A5370 /* GeneralSettingSelectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917072A4B0B16008A5370 /* GeneralSettingSelectionCell.swift */; };
D8F9170B2A4B2C80008A5370 /* AboutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F9170A2A4B2C80008A5370 /* AboutSettings.swift */; };
D8F9170D2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F9170C2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift */; };
D8F917112A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917102A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift */; };
D8F917122A4C6B67008A5370 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8318A832A4468A800C0FB73 /* GeneralSettingsViewController.swift */; };
D8F917142A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F917132A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift */; };
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; settings = {ATTRIBUTES = (codegen, ); }; };
DB0009A726AEE5DC009B9D2D /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = DB0009A926AEE5DC009B9D2D /* Intents.intentdefinition */; };
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB023D25279FFB0A005AC798 /* ShareActivityProvider.swift */; };
@ -244,7 +261,6 @@
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; };
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; };
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
@ -337,9 +353,6 @@
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; };
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; };
DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; };
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; };
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; };
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; };
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; };
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; };
DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */; };
@ -350,7 +363,6 @@
DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */; };
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */; };
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; };
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; };
DB848E33282B62A800A302CC /* ReportResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB848E32282B62A800A302CC /* ReportResultView.swift */; };
DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */; };
DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */; };
@ -384,8 +396,6 @@
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */; };
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */; };
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
@ -718,11 +728,6 @@
46DAB0EBDDFB678347CD96FF /* Pods-MastodonTests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk - release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk - release.xcconfig"; sourceTree = "<group>"; };
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
5B24BBD8262DB14800A9381B /* ReportStatusViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+Diffable.swift"; sourceTree = "<group>"; };
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = "<group>"; };
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsLinkTableViewCell.swift; sourceTree = "<group>"; };
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSectionHeader.swift; sourceTree = "<group>"; };
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = "<group>"; };
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = "<group>"; };
5CE45680252519F42FEA2D13 /* Pods-ShareActionExtension.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - release.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - release.xcconfig"; sourceTree = "<group>"; };
@ -775,10 +780,19 @@
D81A22742AB4643200905D71 /* SearchResultsOverviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsOverviewTableViewController.swift; sourceTree = "<group>"; };
D81A22772AB4782400905D71 /* SearchResultOverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewSection.swift; sourceTree = "<group>"; };
D81A227A2AB47B9A00905D71 /* SearchResultDefaultSectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultSectionTableViewCell.swift; sourceTree = "<group>"; };
D81D12452A4E1861005009D4 /* PolicySelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicySelectionViewController.swift; sourceTree = "<group>"; };
D81D124A2A4E1914005009D4 /* ToggleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleTableViewCell.swift; sourceTree = "<group>"; };
D82463522A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/Intents.strings; sourceTree = "<group>"; };
D82463532A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/WidgetExtension.strings; sourceTree = "<group>"; };
D82463542A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = "<group>"; };
D82463552A52B47B00A3DBDD /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = be; path = be.lproj/Intents.stringsdict; sourceTree = "<group>"; };
D82BD7512ABC42D6009A374A /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
D82BD7542ABC73AF009A374A /* NotificationPolicyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPolicyTableViewCell.swift; sourceTree = "<group>"; };
D8318A7F2A4466D300C0FB73 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
D8318A832A4468A800C0FB73 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = "<group>"; };
D8318A852A4468C700C0FB73 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = "<group>"; };
D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = "<group>"; };
D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = "<group>"; tabWidth = 4; };
D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = "<group>"; };
D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = "<group>"; };
@ -796,15 +810,29 @@
D8A6FE6429325F5900666A47 /* StringsConvertor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = StringsConvertor; sourceTree = "<group>"; };
D8A6FE6529325F5900666A47 /* ios-infoPlist.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "ios-infoPlist.json"; sourceTree = "<group>"; };
D8A6FE6629325F5900666A47 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewToggleCell.swift; sourceTree = "<group>"; };
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingTableViewCell.swift; sourceTree = "<group>"; };
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; sourceTree = "<group>"; };
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = "<group>"; };
D8BEBCB52A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionAccountTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = "<group>"; };
D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = "<group>"; };
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
D8E5C348296DB8A3007E76A7 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsDisabledTableViewCell.swift; sourceTree = "<group>"; };
D8F0372B29D232730027DE2E /* HashtagIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagIntentHandler.swift; sourceTree = "<group>"; };
D8F8A03929CA5C15000195DD /* HashtagWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidgetView.swift; sourceTree = "<group>"; };
D8F8A03B29CA5CB6000195DD /* HashtagWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagWidget.swift; sourceTree = "<group>"; };
D8F917002A4AD8A4008A5370 /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = "<group>"; };
D8F917022A4B063D008A5370 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
D8F917052A4B0791008A5370 /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = "<group>"; };
D8F917072A4B0B16008A5370 /* GeneralSettingSelectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingSelectionCell.swift; sourceTree = "<group>"; };
D8F9170A2A4B2C80008A5370 /* AboutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettings.swift; sourceTree = "<group>"; };
D8F9170C2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutMastodonTableViewCell.swift; sourceTree = "<group>"; };
D8F9170E2A4B47EF008A5370 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
D8F917102A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingToggleTableViewCell.swift; sourceTree = "<group>"; };
D8F917132A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsDiffableTableViewDataSource.swift; sourceTree = "<group>"; };
DB0009A826AEE5DC009B9D2D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intents.intentdefinition; sourceTree = "<group>"; };
DB0009AD26AEE5E4009B9D2D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = "<group>"; };
DB023D25279FFB0A005AC798 /* ShareActivityProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareActivityProvider.swift; sourceTree = "<group>"; };
@ -903,7 +931,6 @@
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = "<group>"; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
@ -1026,9 +1053,6 @@
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = "<group>"; };
@ -1039,7 +1063,6 @@
DB7A9F902818EAF10016AF98 /* MastodonRegisterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterView.swift; sourceTree = "<group>"; };
DB7A9F922818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonServerRulesViewController+Debug.swift"; sourceTree = "<group>"; };
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = "<group>"; };
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = "<group>"; };
DB848E32282B62A800A302CC /* ReportResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultView.swift; sourceTree = "<group>"; };
DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = "<group>"; };
DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = "<group>"; };
@ -1089,8 +1112,6 @@
DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewController.swift; sourceTree = "<group>"; };
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = "<group>"; };
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsAppearanceTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
@ -1612,7 +1633,6 @@
DB0617F727855B010030EE79 /* Notification */,
DB4F097726A039A200D62E92 /* Search */,
DB3E6FE52806A5BA00B035AE /* Discovery */,
DB0617FA27855B660030EE79 /* Settings */,
);
path = Diffable;
sourceTree = "<group>";
@ -1730,34 +1750,16 @@
5B90C455262599800002E742 /* Settings */ = {
isa = PBXGroup;
children = (
5B90C458262599800002E742 /* Cell */,
5B90C457262599800002E742 /* View */,
DB6D9F9626367249008423CD /* SettingsViewController.swift */,
5B90C456262599800002E742 /* SettingsViewModel.swift */,
D81D12492A4E190A005009D4 /* Shared */,
D8F916FF2A4AD898008A5370 /* Settings Overview */,
D8F917042A4B0657008A5370 /* General Settings */,
D81D12432A4E181C005009D4 /* Notification Settings */,
D8F917092A4B2AFF008A5370 /* About Mastodon */,
D8318A7F2A4466D300C0FB73 /* SettingsCoordinator.swift */,
);
path = Settings;
sourceTree = "<group>";
};
5B90C457262599800002E742 /* View */ = {
isa = PBXGroup;
children = (
5B90C45C262599800002E742 /* SettingsSectionHeader.swift */,
DB443CD32694627B00159B29 /* AppearanceView.swift */,
);
path = View;
sourceTree = "<group>";
};
5B90C458262599800002E742 /* Cell */ = {
isa = PBXGroup;
children = (
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */,
DB98EB6A27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift */,
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */,
5B90C45B262599800002E742 /* SettingsLinkTableViewCell.swift */,
);
path = Cell;
sourceTree = "<group>";
};
5D03938E2612D200007FE196 /* Webview */ = {
isa = PBXGroup;
children = (
@ -1809,6 +1811,26 @@
path = Cells;
sourceTree = "<group>";
};
D81D12432A4E181C005009D4 /* Notification Settings */ = {
isa = PBXGroup;
children = (
D8ECC80D2AC31E0F00AE0818 /* Policy Selection */,
D8ECC80E2AC31E2C00AE0818 /* Cells */,
D8318A872A4468D300C0FB73 /* NotificationSettingsViewController.swift */,
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */,
D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */,
);
path = "Notification Settings";
sourceTree = "<group>";
};
D81D12492A4E190A005009D4 /* Shared */ = {
isa = PBXGroup;
children = (
D81D124A2A4E1914005009D4 /* ToggleTableViewCell.swift */,
);
path = Shared;
sourceTree = "<group>";
};
D8A6AB68291C50F3003AB663 /* Login */ = {
isa = PBXGroup;
children = (
@ -1845,6 +1867,25 @@
path = "Edit History";
sourceTree = "<group>";
};
D8ECC80D2AC31E0F00AE0818 /* Policy Selection */ = {
isa = PBXGroup;
children = (
D81D12452A4E1861005009D4 /* PolicySelectionViewController.swift */,
D82BD7542ABC73AF009A374A /* NotificationPolicyTableViewCell.swift */,
);
path = "Policy Selection";
sourceTree = "<group>";
};
D8ECC80E2AC31E2C00AE0818 /* Cells */ = {
isa = PBXGroup;
children = (
D8B5E4ED2A4EB8920008970C /* NotificationSettingTableViewToggleCell.swift */,
D8B5E4EF2A4EB8A00008970C /* NotificationSettingTableViewCell.swift */,
D8ECC80F2AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift */,
);
path = Cells;
sourceTree = "<group>";
};
D8F8A03829CA5C02000195DD /* Hashtag */ = {
isa = PBXGroup;
children = (
@ -1854,6 +1895,38 @@
path = Hashtag;
sourceTree = "<group>";
};
D8F916FF2A4AD898008A5370 /* Settings Overview */ = {
isa = PBXGroup;
children = (
D8F917002A4AD8A4008A5370 /* SettingsTableViewCell.swift */,
D8318A852A4468C700C0FB73 /* SettingsViewController.swift */,
D8F917022A4B063D008A5370 /* Settings.swift */,
);
path = "Settings Overview";
sourceTree = "<group>";
};
D8F917042A4B0657008A5370 /* General Settings */ = {
isa = PBXGroup;
children = (
D8318A832A4468A800C0FB73 /* GeneralSettingsViewController.swift */,
D8F917052A4B0791008A5370 /* GeneralSettings.swift */,
D8F917102A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift */,
D8F917072A4B0B16008A5370 /* GeneralSettingSelectionCell.swift */,
D8F917132A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift */,
);
path = "General Settings";
sourceTree = "<group>";
};
D8F917092A4B2AFF008A5370 /* About Mastodon */ = {
isa = PBXGroup;
children = (
D8318A892A4468DC00C0FB73 /* AboutViewController.swift */,
D8F9170A2A4B2C80008A5370 /* AboutSettings.swift */,
D8F9170C2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift */,
);
path = "About Mastodon";
sourceTree = "<group>";
};
DB01409B25C40BB600F9F3CF /* Onboarding */ = {
isa = PBXGroup;
children = (
@ -1897,15 +1970,6 @@
path = Profile;
sourceTree = "<group>";
};
DB0617FA27855B660030EE79 /* Settings */ = {
isa = PBXGroup;
children = (
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
);
path = Settings;
sourceTree = "<group>";
};
DB0618082785B2790030EE79 /* Cell */ = {
isa = PBXGroup;
children = (
@ -2168,8 +2232,6 @@
DB55D32225FB4D320002F825 /* View */ = {
isa = PBXGroup;
children = (
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */,
);
path = View;
@ -2476,6 +2538,7 @@
children = (
DB8AF54225C13647002E6C99 /* SceneCoordinator.swift */,
DB8AF54325C13647002E6C99 /* NeedsDependency.swift */,
D8F9170E2A4B47EF008A5370 /* Coordinator.swift */,
);
path = Coordinator;
sourceTree = "<group>";
@ -2578,6 +2641,7 @@
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */,
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */,
D82BD7512ABC42D6009A374A /* Coordinator.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
@ -3395,7 +3459,6 @@
};
DB025B8E278D6448002F581E /* Run Sourcery: Core Data */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 12;
files = (
);
@ -3414,7 +3477,6 @@
};
DB3D100425BAA71500EAA174 /* Run SwiftGen */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 12;
files = (
);
@ -3433,7 +3495,6 @@
};
DB697DD2278F48D5004EF2F7 /* Run Sourcery */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 12;
files = (
);
@ -3563,7 +3624,6 @@
2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */,
D8099078294BC8A30050219F /* PrivacyTableViewController.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */,
DBF1D24E269DAF5D00C1C08A /* SearchDetailViewController.swift in Sources */,
62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */,
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
@ -3590,6 +3650,7 @@
DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */,
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,
DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */,
D81D12462A4E1861005009D4 /* PolicySelectionViewController.swift in Sources */,
DB848E33282B62A800A302CC /* ReportResultView.swift in Sources */,
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
DB0FCB9C27980AB6006C02E2 /* HashtagTimelineViewController+DataSourceProvider.swift in Sources */,
@ -3634,17 +3695,18 @@
DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */,
DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */,
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */,
5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */,
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */,
DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */,
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */,
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
DB5B54B02833C24200DEF8B2 /* FavoritedByViewController+DataSourceProvider.swift in Sources */,
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
DB159C2B27A17BAC0068DC77 /* DataSourceFacade+Media.swift in Sources */,
D8F917082A4B0B16008A5370 /* GeneralSettingSelectionCell.swift in Sources */,
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
@ -3661,9 +3723,9 @@
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */,
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */,
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */,
DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */,
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */,
@ -3672,6 +3734,7 @@
DB5B549D2833A67400DEF8B2 /* FamiliarFollowersViewModel.swift in Sources */,
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */,
D8B5E4F02A4EB8A00008970C /* NotificationSettingTableViewCell.swift in Sources */,
DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */,
DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */,
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
@ -3683,13 +3746,15 @@
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */,
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */,
DB5B54A12833A89600DEF8B2 /* FamiliarFollowersViewController+DataSourceProvider.swift in Sources */,
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
DB025B78278D606A002F581E /* StatusItem.swift in Sources */,
D82BD7532ABC44C2009A374A /* Coordinator.swift in Sources */,
DB697DD4278F4927004EF2F7 /* StatusTableViewCellDelegate.swift in Sources */,
2A506CF6292D040100059C37 /* HashtagTimelineHeaderView.swift in Sources */,
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
D81D124B2A4E1914005009D4 /* ToggleTableViewCell.swift in Sources */,
DB3E6FF52807C40300B035AE /* DiscoveryForYouViewController.swift in Sources */,
D809907A294BC9390050219F /* PrivacyTableViewCell.swift in Sources */,
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
@ -3713,17 +3778,15 @@
DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */,
DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */,
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */,
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */,
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */,
DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */,
5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */,
D8F917032A4B063D008A5370 /* Settings.swift in Sources */,
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */,
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
@ -3750,11 +3813,12 @@
DBDFF197280556D900557A48 /* DiscoveryPostsViewModel+State.swift in Sources */,
DB0FCB7A279576A2006C02E2 /* DataSourceFacade+Thread.swift in Sources */,
DB9F58EF26EF491E00E7BBE9 /* AccountListViewModel.swift in Sources */,
DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */,
DB0FCB9A2797F7AD006C02E2 /* UserView+Configuration.swift in Sources */,
DB023D2827A0FABD005AC798 /* NotificationTableViewCellDelegate.swift in Sources */,
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
DB023D2C27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */,
D8F917062A4B0791008A5370 /* GeneralSettings.swift in Sources */,
D8F917012A4AD8A5008A5370 /* SettingsTableViewCell.swift in Sources */,
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */,
DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */,
DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */,
@ -3769,6 +3833,7 @@
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */,
DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */,
D8F9170B2A4B2C80008A5370 /* AboutSettings.swift in Sources */,
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */,
2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */,
@ -3789,7 +3854,6 @@
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
6213AF5C28939C8A00BCADB6 /* BookmarkViewModel+State.swift in Sources */,
D807C6C029DE197900A4E17C /* EducationViewController.swift in Sources */,
DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */,
2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */,
DBEFCD7B282A162400C0ABEA /* ReportReasonView.swift in Sources */,
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */,
@ -3805,6 +3869,7 @@
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */,
DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */,
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
D8F9170D2A4B3C6F008A5370 /* AboutMastodonTableViewCell.swift in Sources */,
DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */,
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */,
@ -3828,6 +3893,7 @@
DB4F097D26A03A5B00D62E92 /* SearchHistoryItem.swift in Sources */,
DBD5B1FA27BD013700BD6B38 /* DataSourceProvider+StatusTableViewControllerNavigateable.swift in Sources */,
D809907C294D25510050219F /* PrivacyViewModel.swift in Sources */,
D8318A862A4468C700C0FB73 /* SettingsViewController.swift in Sources */,
DB5B549A2833A60400DEF8B2 /* FamiliarFollowersViewController.swift in Sources */,
DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */,
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
@ -3841,7 +3907,6 @@
DBFEEC96279BDC67004F81DD /* ProfileAboutViewController.swift in Sources */,
DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */,
DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */,
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */,
DB63F74D27993F5B00455B82 /* SearchHistoryUserCollectionViewCell.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
@ -3852,14 +3917,15 @@
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
D8BEBCB62A1B7FFD0004F475 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */,
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */,
DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */,
DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */,
D8F917142A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift in Sources */,
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
DB1D842E26552C4D000346B3 /* StatusTableViewControllerNavigateable.swift in Sources */,
DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */,
DB98EB6B27B243470082E365 /* SettingsAppearanceTableViewCell+ViewModel.swift in Sources */,
DBB45B5B27B3A109002DC5A7 /* MediaPreviewTransitionViewController.swift in Sources */,
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */,
@ -3867,7 +3933,6 @@
DB697DDD278F521D004EF2F7 /* DataSourceFacade.swift in Sources */,
DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */,
DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */,
DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */,
DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */,
DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */,
DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */,
@ -3883,6 +3948,7 @@
2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */,
DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */,
DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */,
D8318A802A4466D300C0FB73 /* SettingsCoordinator.swift in Sources */,
DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */,
DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */,
DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */,
@ -3890,7 +3956,10 @@
DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */,
DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */,
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */,
D8F917112A4C6B40008A5370 /* GeneralSettingToggleTableViewCell.swift in Sources */,
D8B5E4EE2A4EB8930008970C /* NotificationSettingTableViewToggleCell.swift in Sources */,
DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */,
D8318A8A2A4468DC00C0FB73 /* AboutViewController.swift in Sources */,
85BC11B32932414900E191CD /* AltTextViewController.swift in Sources */,
DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */,
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */,
@ -3899,11 +3968,13 @@
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */,
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
D8F917122A4C6B67008A5370 /* GeneralSettingsViewController.swift in Sources */,
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */,
855149CA29606D6400943D96 /* PortraitAlertController.swift in Sources */,
DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */,
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */,
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */,
D8ECC8102AC31EA400AE0818 /* NotificationSettingsDisabledTableViewCell.swift in Sources */,
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
DBEFCD82282A2AB100C0ABEA /* ReportServerRulesView.swift in Sources */,
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,

View File

@ -0,0 +1,7 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
protocol Coordinator {
func start()
}

View File

@ -30,6 +30,9 @@ final public class SceneCoordinator {
private(set) weak var splitViewController: RootSplitViewController?
private(set) var secondaryStackHashValues = Set<Int>()
var childCoordinator: Coordinator?
private var mastodonAuthenticationController: MastodonAuthenticationController?
init(
scene: UIScene,
@ -95,18 +98,18 @@ final public class SceneCoordinator {
let notificationID = String(pushNotification.notificationID)
switch type {
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other:
assertionFailure()
break
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, authContext: authContext, notificationID: notificationID)
_ = self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other:
assertionFailure()
break
}
} // end DispatchQueue.main.async
@ -133,6 +136,7 @@ extension SceneCoordinator {
case safariPresent(animated: Bool, completion: (() -> Void)? = nil)
case alertController(animated: Bool, completion: (() -> Void)? = nil)
case activityViewControllerPresent(animated: Bool, completion: (() -> Void)? = nil)
case none
}
enum Scene {
@ -161,7 +165,7 @@ extension SceneCoordinator {
// Hashtag Timeline
case hashtagTimeline(viewModel: HashtagTimelineViewModel)
// profile
case accountList(viewModel: AccountListViewModel)
case profile(viewModel: ProfileViewModel)
@ -175,7 +179,7 @@ extension SceneCoordinator {
case followedTags(viewModel: FollowedTagsViewModel)
// setting
case settings(viewModel: SettingsViewModel)
case settings(setting: Setting)
// report
case report(viewModel: ReportViewModel)
@ -197,19 +201,19 @@ extension SceneCoordinator {
var isOnboarding: Bool {
switch self {
case .welcome,
.mastodonPickServer,
.mastodonRegister,
.mastodonLogin,
.mastodonServerRules,
.mastodonConfirmEmail,
.mastodonResendEmail:
return true
default:
return false
case .welcome,
.mastodonPickServer,
.mastodonRegister,
.mastodonLogin,
.mastodonServerRules,
.mastodonConfirmEmail,
.mastodonResendEmail:
return true
default:
return false
}
}
} // end enum Scene { }
} // end enum Scene { }
}
extension SceneCoordinator {
@ -223,16 +227,16 @@ extension SceneCoordinator {
self.authContext = _authContext
switch UIDevice.current.userInterfaceIdiom {
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
case .phone:
let viewController = MainTabBarController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = nil
self.tabBarController = viewController
rootViewController = viewController
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self, authContext: _authContext)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
rootViewController = splitViewController
}
sceneDelegate.window?.rootViewController = rootViewController // base: main
@ -258,7 +262,7 @@ extension SceneCoordinator {
@MainActor
@discardableResult
func present(scene: Scene, from sender: UIViewController? = nil, transition: Transition) -> UIViewController? {
guard let viewController = get(scene: scene) else {
guard let viewController = get(scene: scene, from: sender) else {
return nil
}
guard var presentingViewController = sender ?? sceneDelegate.window?.rootViewController?.topMost else {
@ -267,16 +271,16 @@ extension SceneCoordinator {
// adapt for child controller
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
switch viewController {
case is ProfileViewController:
let title: String = {
let title = navigationControllerVisibleViewController.navigationItem.title ?? ""
return title.count > 10 ? "" : title
}()
let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
default:
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = nil
case is ProfileViewController:
let title: String = {
let title = navigationControllerVisibleViewController.navigationItem.title ?? ""
return title.count > 10 ? "" : title
}()
let barButtonItem = UIBarButtonItem(title: title, style: .plain, target: nil, action: nil)
barButtonItem.tintColor = .white
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
default:
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = nil
}
}
@ -287,69 +291,72 @@ extension SceneCoordinator {
}
switch transition {
case .show:
presentingViewController.show(viewController, sender: sender)
case .showDetail:
secondaryStackHashValues.insert(viewController.hashValue)
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
presentingViewController.showDetailViewController(navigationController, sender: sender)
case .modal(let animated, let completion):
let modalNavigationController: UINavigationController = {
if scene.isOnboarding {
return OnboardingNavigationController(rootViewController: viewController)
} else {
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
}
}()
modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
}
presentingViewController.present(modalNavigationController, animated: animated, completion: completion)
case .none:
// do nothing
break
case .show:
presentingViewController.show(viewController, sender: sender)
case .showDetail:
secondaryStackHashValues.insert(viewController.hashValue)
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
presentingViewController.showDetailViewController(navigationController, sender: sender)
case .panModal:
guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else {
assertionFailure()
return nil
}
// https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441
panModalPresentable.modalPresentationStyle = .custom
panModalPresentable.modalPresentationCapturesStatusBarAppearance = true
panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default
presentingViewController.present(panModalPresentable, animated: true, completion: nil)
//presentingViewController.presentPanModal(panModalPresentable)
case .popover(let sourceView):
viewController.modalPresentationStyle = .popover
viewController.popoverPresentationController?.sourceView = sourceView
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
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):
// set delegate in view controller
assert(sender?.navigationController?.delegate != nil)
sender?.navigationController?.pushViewController(viewController, animated: animated)
case .safariPresent(let animated, let completion):
if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
case .modal(let animated, let completion):
let modalNavigationController: UINavigationController = {
if scene.isOnboarding {
return OnboardingNavigationController(rootViewController: viewController)
} else {
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
}
}()
modalNavigationController.modalPresentationCapturesStatusBarAppearance = true
if let adaptivePresentationControllerDelegate = viewController as? UIAdaptivePresentationControllerDelegate {
modalNavigationController.presentationController?.delegate = adaptivePresentationControllerDelegate
}
presentingViewController.present(modalNavigationController, animated: animated, completion: completion)
case .panModal:
guard let panModalPresentable = viewController as? PanModalPresentable & UIViewController else {
assertionFailure()
return nil
}
// https://github.com/slackhq/PanModal/issues/74#issuecomment-572426441
panModalPresentable.modalPresentationStyle = .custom
panModalPresentable.modalPresentationCapturesStatusBarAppearance = true
panModalPresentable.transitioningDelegate = PanModalPresentationDelegate.default
presentingViewController.present(panModalPresentable, animated: true, completion: nil)
//presentingViewController.presentPanModal(panModalPresentable)
case .popover(let sourceView):
viewController.modalPresentationStyle = .popover
viewController.popoverPresentationController?.sourceView = sourceView
(splitViewController ?? presentingViewController)?.present(viewController, animated: true, completion: nil)
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):
// set delegate in view controller
assert(sender?.navigationController?.delegate != nil)
sender?.navigationController?.pushViewController(viewController, animated: animated)
case .safariPresent(let animated, let completion):
if UserDefaults.shared.preferredUsingDefaultBrowser, case let .safari(url) = scene {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
}
case .alertController(let animated, let completion):
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
case .activityViewControllerPresent(let animated, let completion):
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
}
case .alertController(let animated, let completion):
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
case .activityViewControllerPresent(let animated, let completion):
viewController.modalPresentationCapturesStatusBarAppearance = true
presentingViewController.present(viewController, animated: animated, completion: completion)
}
return viewController
@ -368,7 +375,7 @@ extension SceneCoordinator {
private extension SceneCoordinator {
func get(scene: Scene) -> UIViewController? {
func get(scene: Scene, from sender: UIViewController? = nil) -> UIViewController? {
let viewController: UIViewController?
switch scene {
@ -409,8 +416,6 @@ private extension SceneCoordinator {
let _viewController = WebViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .searchDetail(let viewModel):
let _viewController = SearchDetailViewController(appContext: appContext, sceneCoordinator: self, authContext: viewModel.authContext)
_viewController.viewModel = viewModel
@ -421,8 +426,6 @@ private extension SceneCoordinator {
searchResultViewController.coordinator = self
searchResultViewController.viewModel = viewModel
viewController = searchResultViewController
case .compose(let viewModel):
let _viewController = ComposeViewController(viewModel: viewModel)
viewController = _viewController
@ -511,10 +514,10 @@ private extension SceneCoordinator {
return nil
}
let _viewController = SFSafariViewController(url: url)
_viewController.preferredBarTintColor = ThemeService.shared.currentTheme.value.navigationBarBackgroundColor
_viewController.preferredBarTintColor = SystemTheme.navigationBarBackgroundColor
_viewController.preferredControlTintColor = Asset.Colors.Brand.blurple.color
viewController = _viewController
case .alertController(let alertController):
if let popoverPresentationController = alertController.popoverPresentationController {
assert(
@ -528,10 +531,23 @@ private extension SceneCoordinator {
activityViewController.popoverPresentationController?.sourceView = sourceView
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
viewController = activityViewController
case .settings(let viewModel):
let _viewController = SettingsViewController()
_viewController.viewModel = viewModel
viewController = _viewController
case .settings(let setting):
guard let presentedOn = sender,
let accountName = authContext?.mastodonAuthenticationBox.authenticationRecord.object(in: appContext.managedObjectContext)?.username,
let authContext
else { return nil }
let settingsCoordinator = SettingsCoordinator(presentedOn: presentedOn,
accountName: accountName,
setting: setting,
appContext: appContext,
authContext: authContext)
settingsCoordinator.delegate = self
settingsCoordinator.start()
viewController = settingsCoordinator.navigationController
childCoordinator = settingsCoordinator
case .editStatus(let viewModel):
let composeViewController = ComposeViewController(viewModel: viewModel)
viewController = composeViewController
@ -551,11 +567,83 @@ private extension SceneCoordinator {
//MARK: - MastodonLoginViewControllerDelegate
extension SceneCoordinator: MastodonLoginViewControllerDelegate {
func backButtonPressed(_ viewController: MastodonLoginViewController) {
viewController.navigationController?.popViewController(animated: true)
}
func backButtonPressed(_ viewController: MastodonLoginViewController) {
viewController.navigationController?.popViewController(animated: true)
}
func nextButtonPressed(_ viewController: MastodonLoginViewController) {
viewController.login()
}
func nextButtonPressed(_ viewController: MastodonLoginViewController) {
viewController.login()
}
}
//MARK: - SettingsCoordinatorDelegate
extension SceneCoordinator: SettingsCoordinatorDelegate {
func logout(_ settingsCoordinator: SettingsCoordinator) {
let alertController = UIAlertController(
title: L10n.Common.Alerts.SignOut.title,
message: L10n.Common.Alerts.SignOut.message,
preferredStyle: .actionSheet
)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
let signOutAction = UIAlertAction(title: L10n.Common.Alerts.SignOut.confirm, style: .destructive) { [weak self] _ in
guard let self, let authContext = self.authContext else { return }
self.appContext.notificationService.clearNotificationCountForActiveUser()
Task { @MainActor in
try await self.appContext.authenticationService.signOutMastodonUser(
authenticationBox: authContext.mastodonAuthenticationBox
)
self.setup()
}
}
alertController.addAction(cancelAction)
alertController.addAction(signOutAction)
settingsCoordinator.navigationController.present(alertController, animated: true)
}
@MainActor
func openGithubURL(_ settingsCoordinator: SettingsCoordinator) {
guard let githubURL = URL(string: "https://github.com/mastodon/mastodon-ios") else { return }
_ = present(
scene: .safari(url: githubURL),
from: settingsCoordinator.navigationController,
transition: .safariPresent(animated: true)
)
}
@MainActor
func openPrivacyURL(_ settingsCoordinator: SettingsCoordinator) {
guard let authContext else { return }
let domain = authContext.mastodonAuthenticationBox.domain
let privacyURL = Mastodon.API.privacyURL(domain: domain)
_ = present(scene: .safari(url: privacyURL),
from: settingsCoordinator.navigationController,
transition: .safariPresent(animated: true))
}
func openProfileSettingsURL(_ settingsCoordinator: SettingsCoordinator) {
guard let authContext else { return }
let domain = authContext.mastodonAuthenticationBox.domain
let profileSettingsURL = Mastodon.API.profileSettingsURL(domain: domain)
let authenticationController = MastodonAuthenticationController(context: appContext, authenticateURL: profileSettingsURL)
authenticationController.authenticationSession?.presentationContextProvider = settingsCoordinator
authenticationController.authenticationSession?.start()
self.mastodonAuthenticationController = authenticationController
}
}

View File

@ -1,115 +0,0 @@
//
// SettingsItem.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-25.
//
import UIKit
import CoreData
import CoreDataStack
import MastodonAsset
import MastodonLocalization
enum SettingsItem {
case appearance(record: ManagedObjectRecord<Setting>)
case preference(settingRecord: ManagedObjectRecord<Setting>, preferenceType: PreferenceType)
case notification(settingRecord: ManagedObjectRecord<Setting>, switchMode: NotificationSwitchMode)
case boringZone(item: Link)
case spicyZone(item: Link)
}
extension SettingsItem {
enum AppearanceMode: String {
case system
case dark
case light
}
enum NotificationSwitchMode: CaseIterable, Hashable {
case favorite
case follow
case reblog
case mention
var title: String {
switch self {
case .favorite: return L10n.Scene.Settings.Section.Notifications.favorites
case .follow: return L10n.Scene.Settings.Section.Notifications.follows
case .reblog: return L10n.Scene.Settings.Section.Notifications.boosts
case .mention: return L10n.Scene.Settings.Section.Notifications.mentions
}
}
}
enum PreferenceType: CaseIterable {
case disableAvatarAnimation
case disableEmojiAnimation
case useDefaultBrowser
var title: String {
switch self {
case .disableAvatarAnimation: return L10n.Scene.Settings.Section.Preference.disableAvatarAnimation
case .disableEmojiAnimation: return L10n.Scene.Settings.Section.Preference.disableEmojiAnimation
case .useDefaultBrowser: return L10n.Scene.Settings.Section.Preference.usingDefaultBrowser
}
}
}
enum Link: CaseIterable, Hashable {
case accountSettings
case github
case termsOfService
case privacyPolicy
case clearMediaCache
case signOut
var title: String {
switch self {
case .accountSettings: return L10n.Scene.Settings.Section.BoringZone.accountSettings
case .github: return "GitHub"
case .termsOfService: return L10n.Scene.Settings.Section.BoringZone.terms
case .privacyPolicy: return L10n.Scene.Settings.Section.BoringZone.privacy
case .clearMediaCache: return L10n.Scene.Settings.Section.SpicyZone.clear
case .signOut: return L10n.Scene.Settings.Section.SpicyZone.signout
}
}
var textColor: UIColor? {
switch self {
case .accountSettings: return nil // tintColor
case .github: return nil
case .termsOfService: return nil
case .privacyPolicy: return nil
case .clearMediaCache: return .systemRed
case .signOut: return .systemRed
}
}
}
}
extension SettingsItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .appearance(let record):
hasher.combine(String(describing: SettingsItem.AppearanceMode.self))
hasher.combine(record)
case .notification(let settingObjectID, let switchMode):
hasher.combine(String(describing: SettingsItem.notification.self))
hasher.combine(settingObjectID)
hasher.combine(switchMode)
case .preference(let settingObjectID, let preferenceType):
hasher.combine(String(describing: SettingsItem.preference.self))
hasher.combine(settingObjectID)
hasher.combine(preferenceType)
case .boringZone(let link):
hasher.combine(String(describing: SettingsItem.boringZone.self))
hasher.combine(link)
case .spicyZone(let link):
hasher.combine(String(describing: SettingsItem.spicyZone.self))
hasher.combine(link)
}
}
}

View File

@ -1,142 +0,0 @@
//
// SettingsSection.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-25.
//
import UIKit
import CoreData
import CoreDataStack
import MastodonAsset
import MastodonCore
import MastodonLocalization
enum SettingsSection: Hashable {
case appearance
case preference
case notifications
case boringZone
case spicyZone
var title: String {
switch self {
case .appearance: return L10n.Scene.Settings.Section.LookAndFeel.title
case .preference: return ""
case .notifications: return L10n.Scene.Settings.Section.Notifications.title
case .boringZone: return L10n.Scene.Settings.Section.BoringZone.title
case .spicyZone: return L10n.Scene.Settings.Section.SpicyZone.title
}
}
}
extension SettingsSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
managedObjectContext: NSManagedObjectContext,
settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
settingsToggleCellDelegate: SettingsToggleCellDelegate
) -> UITableViewDiffableDataSource<SettingsSection, SettingsItem> {
UITableViewDiffableDataSource(tableView: tableView) { [
weak settingsAppearanceTableViewCellDelegate,
weak settingsToggleCellDelegate
] tableView, indexPath, item -> UITableViewCell? in
switch item {
case .appearance:
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsAppearanceTableViewCell.self), for: indexPath) as! SettingsAppearanceTableViewCell
cell.delegate = settingsAppearanceTableViewCellDelegate
return cell
case .preference(let record, _):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
cell.delegate = settingsToggleCellDelegate
managedObjectContext.performAndWait {
guard let setting = record.object(in: managedObjectContext) else { return }
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
ManagedObjectObserver.observe(object: setting)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in
// do nothing
}, receiveValue: { [weak cell] change in
guard let cell = cell else { return }
guard case .update(let object) = change.changeType,
let setting = object as? Setting else { return }
SettingsSection.configureSettingToggle(cell: cell, item: item, setting: setting)
})
.store(in: &cell.disposeBag)
}
return cell
case .notification(let record, let switchMode):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsToggleTableViewCell.self), for: indexPath) as! SettingsToggleTableViewCell
managedObjectContext.performAndWait {
guard let setting = record.object(in: managedObjectContext) else { return }
if let subscription = setting.activeSubscription {
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
}
ManagedObjectObserver.observe(object: setting)
.sink(receiveCompletion: { _ in
// do nothing
}, receiveValue: { [weak cell] change in
guard let cell = cell else { return }
guard case .update(let object) = change.changeType,
let setting = object as? Setting else { return }
guard let subscription = setting.activeSubscription else { return }
SettingsSection.configureSettingToggle(cell: cell, switchMode: switchMode, subscription: subscription)
})
.store(in: &cell.disposeBag)
}
cell.delegate = settingsToggleCellDelegate
return cell
case .boringZone(let item),
.spicyZone(let item):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: SettingsLinkTableViewCell.self), for: indexPath) as! SettingsLinkTableViewCell
cell.update(with: item)
return cell
} // end switch
}
}
}
extension SettingsSection {
public static func configureSettingToggle(
cell: SettingsToggleTableViewCell,
item: SettingsItem,
setting: Setting
) {
switch item {
case .preference(_, let preferenceType):
cell.textLabel?.text = preferenceType.title
switch preferenceType {
case .disableAvatarAnimation:
cell.switchButton.isOn = setting.preferredStaticAvatar
case .disableEmojiAnimation:
cell.switchButton.isOn = setting.preferredStaticEmoji
case .useDefaultBrowser:
cell.switchButton.isOn = setting.preferredUsingDefaultBrowser
}
default:
assertionFailure()
}
}
public static func configureSettingToggle(
cell: SettingsToggleTableViewCell,
switchMode: SettingsItem.NotificationSwitchMode,
subscription: NotificationSubscription
) {
cell.textLabel?.text = switchMode.title
let enabled: Bool?
switch switchMode {
case .favorite: enabled = subscription.alert.favourite
case .follow: enabled = subscription.alert.follow
case .reblog: enabled = subscription.alert.reblog
case .mention: enabled = subscription.alert.mention
}
cell.update(enabled: enabled)
}
}

View File

@ -1,4 +1,4 @@
// Generated using Sourcery 1.9.0 https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 1.9.2 https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// sourcery:inline:DiscoveryCommunityViewController.AutoGenerateTableViewDelegate

View File

@ -82,14 +82,7 @@ extension AccountListViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
setupBackgroundColor()
navigationItem.rightBarButtonItem = addBarButtonItem
dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false
@ -133,13 +126,13 @@ extension AccountListViewController {
.store(in: &disposeBag)
}
private func setupBackgroundColor(theme: Theme) {
private func setupBackgroundColor() {
let backgroundColor = UIColor { traitCollection in
switch traitCollection.userInterfaceLevel {
case .elevated where traitCollection.userInterfaceStyle == .dark:
return theme.systemElevatedBackgroundColor
return SystemTheme.systemElevatedBackgroundColor
default:
return theme.systemBackgroundColor.withAlphaComponent(0.9)
return .systemBackground.withAlphaComponent(0.9)
}
}
view.backgroundColor = backgroundColor

View File

@ -50,14 +50,7 @@ final class AccountListTableViewCell: UITableViewCell {
extension AccountListTableViewCell {
private func _init() {
backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
.store(in: &_disposeBag)
backgroundColor = .secondarySystemGroupedBackground
avatarButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(avatarButton)

View File

@ -48,14 +48,7 @@ final class AddAccountTableViewCell: UITableViewCell {
extension AddAccountTableViewCell {
private func _init() {
backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
}
.store(in: &_disposeBag)
backgroundColor = .secondarySystemGroupedBackground
iconImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(iconImageView)

View File

@ -32,7 +32,7 @@ final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionVi
override var isHighlighted: Bool {
didSet {
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? ThemeService.shared.currentTheme.value.tertiarySystemGroupedBackgroundColor.withAlphaComponent(0.6) : ThemeService.shared.currentTheme.value.tertiarySystemGroupedBackgroundColor
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? UIColor.tertiarySystemGroupedBackground.withAlphaComponent(0.6) : .tertiarySystemGroupedBackground
pollOptionView.plusCircleImageView.tintColor = isHighlighted ? Asset.Colors.Brand.blurple.color.withAlphaComponent(0.5) : Asset.Colors.Brand.blurple.color
}
}
@ -85,7 +85,7 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
pollOptionView.optionTextField.isHidden = true
pollOptionView.plusCircleImageView.isHidden = false
pollOptionView.roundedBackgroundView.backgroundColor = ThemeService.shared.currentTheme.value.tertiarySystemGroupedBackgroundColor
pollOptionView.roundedBackgroundView.backgroundColor = .tertiarySystemGroupedBackground
setupBorderColor()
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
@ -96,7 +96,7 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
private func setupBorderColor() {
pollOptionView.roundedBackgroundView.layer.borderWidth = 1
pollOptionView.roundedBackgroundView.layer.borderColor = ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
pollOptionView.roundedBackgroundView.layer.borderColor = SystemTheme.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

View File

@ -87,9 +87,9 @@ extension ComposeStatusPollOptionCollectionViewCell {
pollOptionView.optionPercentageLabel.isHidden = true
pollOptionView.optionTextField.text = nil
pollOptionView.roundedBackgroundView.backgroundColor = ThemeService.shared.currentTheme.value.tertiarySystemGroupedBackgroundColor
pollOptionView.roundedBackgroundView.backgroundColor = .tertiarySystemGroupedBackground
pollOptionView.checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { traitCollection in
return traitCollection.userInterfaceStyle == .light ? .white : ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
return traitCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
})
setupBorderColor()
@ -109,9 +109,9 @@ extension ComposeStatusPollOptionCollectionViewCell {
private func setupBorderColor() {
pollOptionView.roundedBackgroundView.layer.borderWidth = 1
pollOptionView.roundedBackgroundView.layer.borderColor = ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
pollOptionView.roundedBackgroundView.layer.borderColor = SystemTheme.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
pollOptionView.checkmarkBackgroundView.layer.borderColor = ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
pollOptionView.checkmarkBackgroundView.layer.borderColor = SystemTheme.tableViewCellSelectionBackgroundColor.withAlphaComponent(0.3).cgColor
pollOptionView.checkmarkBackgroundView.layer.borderWidth = 1
}

View File

@ -1,137 +0,0 @@
//
// AttachmentContainerView+EmptyStateView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-18.
//
import UIKit
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
//extension AttachmentContainerView {
// final class EmptyStateView: UIView {
//
// static let photoFillSplitImage = Asset.Connectivity.photoFillSplit.image.withRenderingMode(.alwaysTemplate)
// static let videoSplashImage: UIImage = {
// let image = UIImage(systemName: "video.slash")!.withConfiguration(UIImage.SymbolConfiguration(pointSize: 64))
// return image
// }()
//
// let imageView: UIImageView = {
// let imageView = UIImageView()
// imageView.tintColor = Asset.Colors.Label.secondary.color
// imageView.image = AttachmentContainerView.EmptyStateView.photoFillSplitImage
// return imageView
// }()
// let label: UILabel = {
// let label = UILabel()
// label.font = .preferredFont(forTextStyle: .body)
// label.textColor = Asset.Colors.Label.secondary.color
// label.textAlignment = .center
// label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo)
// label.numberOfLines = 2
// label.adjustsFontSizeToFitWidth = true
// label.minimumScaleFactor = 0.3
// return label
// }()
//
// override init(frame: CGRect) {
// super.init(frame: frame)
// _init()
// }
//
// required init?(coder: NSCoder) {
// super.init(coder: coder)
// _init()
// }
//
// }
//}
//extension AttachmentContainerView.EmptyStateView {
// private func _init() {
// layer.masksToBounds = true
// layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
// layer.cornerCurve = .continuous
// backgroundColor = ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor
//
// let stackView = UIStackView()
// stackView.axis = .vertical
// stackView.alignment = .center
// stackView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(stackView)
// NSLayoutConstraint.activate([
// stackView.topAnchor.constraint(equalTo: topAnchor),
// stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
// stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
// stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
// ])
// let topPaddingView = UIView()
// let middlePaddingView = UIView()
// let bottomPaddingView = UIView()
//
// topPaddingView.translatesAutoresizingMaskIntoConstraints = false
// stackView.addArrangedSubview(topPaddingView)
// imageView.translatesAutoresizingMaskIntoConstraints = false
// stackView.addArrangedSubview(imageView)
// NSLayoutConstraint.activate([
// imageView.widthAnchor.constraint(equalToConstant: 92).priority(.defaultHigh),
// imageView.heightAnchor.constraint(equalToConstant: 76).priority(.defaultHigh),
// ])
// imageView.setContentHuggingPriority(.required - 1, for: .vertical)
// middlePaddingView.translatesAutoresizingMaskIntoConstraints = false
// stackView.addArrangedSubview(middlePaddingView)
// stackView.addArrangedSubview(label)
// bottomPaddingView.translatesAutoresizingMaskIntoConstraints = false
// stackView.addArrangedSubview(bottomPaddingView)
// NSLayoutConstraint.activate([
// topPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
// bottomPaddingView.heightAnchor.constraint(equalTo: middlePaddingView.heightAnchor, multiplier: 1.5),
// ])
// }
//}
//#if canImport(SwiftUI) && DEBUG
//import SwiftUI
//
//struct AttachmentContainerView_EmptyStateView_Previews: PreviewProvider {
//
// static var previews: some View {
// Group {
// UIViewPreview(width: 375) {
// let emptyStateView = AttachmentContainerView.EmptyStateView()
// NSLayoutConstraint.activate([
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
// ])
// return emptyStateView
// }
// .previewLayout(.fixed(width: 375, height: 205))
// UIViewPreview(width: 375) {
// let emptyStateView = AttachmentContainerView.EmptyStateView()
// NSLayoutConstraint.activate([
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
// ])
// return emptyStateView
// }
// .preferredColorScheme(.dark)
// .previewLayout(.fixed(width: 375, height: 205))
// UIViewPreview(width: 375) {
// let emptyStateView = AttachmentContainerView.EmptyStateView()
// emptyStateView.imageView.image = AttachmentContainerView.EmptyStateView.videoSplashImage
// emptyStateView.label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video)
//
// NSLayoutConstraint.activate([
// emptyStateView.heightAnchor.constraint(equalToConstant: 205)
// ])
// return emptyStateView
// }
// .previewLayout(.fixed(width: 375, height: 205))
// }
// }
//
//}
//
//#endif

View File

@ -1,177 +0,0 @@
//
// AttachmentContainerView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-17.
//
import UIKit
import SwiftUI
import MastodonUI
//final class AttachmentContainerView: UIView {
//
// static let containerViewCornerRadius: CGFloat = 4
//
// var descriptionBackgroundViewFrameObservation: NSKeyValueObservation?
//
// let activityIndicatorView: UIActivityIndicatorView = {
// let activityIndicatorView = UIActivityIndicatorView(style: .large)
// activityIndicatorView.color = UIColor.white.withAlphaComponent(0.8)
// return activityIndicatorView
// }()
//
// let previewImageView: UIImageView = {
// let imageView = UIImageView()
// imageView.contentMode = .scaleAspectFill
// imageView.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
// imageView.layer.cornerCurve = .continuous
// imageView.layer.masksToBounds = true
// return imageView
// }()
//
// let emptyStateView = AttachmentContainerView.EmptyStateView()
// let descriptionBackgroundView: UIView = {
// let view = UIView()
// view.layer.masksToBounds = true
// view.layer.cornerRadius = AttachmentContainerView.containerViewCornerRadius
// view.layer.cornerCurve = .continuous
// view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
// view.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 5, right: 8)
// return view
// }()
// let descriptionBackgroundGradientLayer: CAGradientLayer = {
// let gradientLayer = CAGradientLayer()
// gradientLayer.colors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.69).cgColor]
// gradientLayer.locations = [0.0, 1.0]
// gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
// gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
// gradientLayer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
// return gradientLayer
// }()
// let descriptionTextView: UITextView = {
// let textView = UITextView()
// textView.showsVerticalScrollIndicator = false
// textView.backgroundColor = .clear
// textView.textColor = .white
// textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15), maximumPointSize: 20)
// textView.placeholder = L10n.Scene.Compose.Attachment.descriptionPhoto
// textView.placeholderColor = UIColor.white.withAlphaComponent(0.6) // force white with alpha for Light/Dark mode
// textView.returnKeyType = .done
// return textView
// }()
//
// private(set) lazy var contentView = AttachmentView(viewModel: viewModel)
// public var viewModel: AttachmentView.ViewModel!
//
// override init(frame: CGRect) {
// super.init(frame: frame)
// _init()
// }
//
// required init?(coder: NSCoder) {
// super.init(coder: coder)
// _init()
// }
//
//}
//extension AttachmentContainerView {
//
// private func _init() {
// let hostingViewController = UIHostingController(rootView: contentView)
// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
// addSubview(hostingViewController.view)
// NSLayoutConstraint.activate([
// hostingViewController.view.topAnchor.constraint(equalTo: topAnchor),
// hostingViewController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
// hostingViewController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
// hostingViewController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
// ])
//
// previewImageView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(previewImageView)
// NSLayoutConstraint.activate([
// previewImageView.topAnchor.constraint(equalTo: topAnchor),
// previewImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
// previewImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
// previewImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
// ])
//
// descriptionBackgroundView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(descriptionBackgroundView)
// NSLayoutConstraint.activate([
// descriptionBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
// descriptionBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
// descriptionBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
// descriptionBackgroundView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.3),
// ])
// descriptionBackgroundView.layer.addSublayer(descriptionBackgroundGradientLayer)
// descriptionBackgroundViewFrameObservation = descriptionBackgroundView.observe(\.bounds, options: [.initial, .new]) { [weak self] _, _ in
// guard let self = self else { return }
// self.descriptionBackgroundGradientLayer.frame = self.descriptionBackgroundView.bounds
// }
//
// descriptionTextView.translatesAutoresizingMaskIntoConstraints = false
// descriptionBackgroundView.addSubview(descriptionTextView)
// NSLayoutConstraint.activate([
// descriptionTextView.leadingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.leadingAnchor),
// descriptionTextView.trailingAnchor.constraint(equalTo: descriptionBackgroundView.layoutMarginsGuide.trailingAnchor),
// descriptionBackgroundView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: descriptionTextView.bottomAnchor),
// descriptionTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 36),
// ])
//
// emptyStateView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(emptyStateView)
// NSLayoutConstraint.activate([
// emptyStateView.topAnchor.constraint(equalTo: topAnchor),
// emptyStateView.leadingAnchor.constraint(equalTo: leadingAnchor),
// emptyStateView.trailingAnchor.constraint(equalTo: trailingAnchor),
// emptyStateView.bottomAnchor.constraint(equalTo: bottomAnchor),
// ])
//
// activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(activityIndicatorView)
// NSLayoutConstraint.activate([
// activityIndicatorView.centerXAnchor.constraint(equalTo: previewImageView.centerXAnchor),
// activityIndicatorView.centerYAnchor.constraint(equalTo: previewImageView.centerYAnchor),
// ])
//
// setupBroader()
//
// emptyStateView.isHidden = true
// activityIndicatorView.hidesWhenStopped = true
// activityIndicatorView.startAnimating()
//
// descriptionTextView.delegate = self
// }
//
//// override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
// super.traitCollectionDidChange(previousTraitCollection)
//
// setupBroader()
// }
//
//}
//
//extension AttachmentContainerView {
//
// private func setupBroader() {
// emptyStateView.layer.borderWidth = 1
// emptyStateView.layer.borderColor = traitCollection.userInterfaceStyle == .dark ? ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor.cgColor : nil
// }
//
//}
//// MARK: - UITextViewDelegate
//extension AttachmentContainerView: UITextViewDelegate {
// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// // let keyboard dismiss when input description with "done" type return key
// if textView === descriptionTextView, text == "\n" {
// textView.resignFirstResponder()
// return false
// }
//
// return true
// }
//}

View File

@ -17,7 +17,7 @@ final class StatusContentWarningEditorView: UIView {
// default hidden
let containerBackgroundView: UIView = {
let view = UIView()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
view.backgroundColor = .secondarySystemBackground
return view
}()

View File

@ -37,14 +37,7 @@ extension DiscoveryCommunityViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -80,14 +80,7 @@ extension DiscoveryViewController {
public override func viewDidLoad() {
super.viewDidLoad()
setupAppearance(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupAppearance(theme: theme)
}
.store(in: &disposeBag)
setupAppearance()
dataSource = viewModel
addBar(
@ -116,9 +109,9 @@ extension DiscoveryViewController {
extension DiscoveryViewController {
private func setupAppearance(theme: Theme) {
view.backgroundColor = theme.secondarySystemBackgroundColor
buttonBarBackgroundView.backgroundColor = theme.systemBackgroundColor
private func setupAppearance() {
view.backgroundColor = .secondarySystemBackground
buttonBarBackgroundView.backgroundColor = .systemBackground
}
}

View File

@ -37,14 +37,7 @@ extension DiscoveryForYouViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -39,14 +39,7 @@ extension DiscoveryHashtagsViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -37,14 +37,7 @@ extension DiscoveryNewsViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -38,14 +38,7 @@ extension DiscoveryPostsViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -53,14 +53,7 @@ extension DiscoveryIntroBannerView {
private func _init() {
preservesSuperviewLayoutMargins = true
setupAppearance(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupAppearance(theme: theme)
}
.store(in: &_disposeBag)
backgroundColor = .systemBackground
closeButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(closeButton)
@ -89,11 +82,3 @@ extension DiscoveryIntroBannerView {
delegate?.discoveryIntroBannerView(self, closeButtonDidPressed: sender)
}
}
extension DiscoveryIntroBannerView {
private func setupAppearance(theme: Theme) {
backgroundColor = theme.systemBackgroundColor
}
}

View File

@ -70,14 +70,7 @@ extension HashtagTimelineViewController {
titleView.update(title: _title, subtitle: nil)
navigationItem.titleView = titleView
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
navigationItem.rightBarButtonItem = composeBarButtonItem
composeBarButtonItem.target = self

View File

@ -48,7 +48,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
let settingBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem()
barButtonItem.tintColor = ThemeService.tintColor
barButtonItem.tintColor = SystemTheme.tintColor
barButtonItem.image = Asset.ObjectsAndTools.gear.image.withRenderingMode(.alwaysTemplate)
barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.settings
return barButtonItem
@ -80,14 +80,8 @@ extension HomeTimelineViewController {
super.viewDidLoad()
title = L10n.Scene.HomeTimeline.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
viewModel.$displaySettingBarButtonItem
.receive(on: DispatchQueue.main)
.sink { [weak self] displaySettingBarButtonItem in
@ -378,8 +372,8 @@ extension HomeTimelineViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
_ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}
@objc private func refreshControlValueChanged(_ sender: RefreshControl) {

View File

@ -72,22 +72,6 @@ final class HomeTimelineNavigationBarTitleViewModel {
}
.store(in: &disposeBag)
// context.statusPublishService.latestPublishingComposeViewModel
// .receive(on: DispatchQueue.main)
// .sink { [weak self] composeViewModel in
// guard let self = self else { return }
// guard let composeViewModel = composeViewModel,
// let state = composeViewModel.publishStateMachine.currentState else {
// self.isPublishingPost.value = false
// self.isPublished.value = false
// return
// }
//
// self.isPublishingPost.value = state is ComposeViewModel.PublishState.Publishing || state is ComposeViewModel.PublishState.Fail
// self.isPublished.value = state is ComposeViewModel.PublishState.Finish
// }
// .store(in: &disposeBag)
Publishers.CombineLatest4(
hasNewPosts.eraseToAnyPublisher(),
isOffline.eraseToAnyPublisher(),

View File

@ -64,13 +64,7 @@ extension NotificationTableViewCell {
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1),
])
notificationView.quoteBackgroundView.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.sink { [weak self] theme in
guard let self = self else { return }
self.notificationView.quoteBackgroundView.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &_disposeBag)
notificationView.quoteBackgroundView.backgroundColor = .secondarySystemBackground
notificationView.delegate = self
}

View File

@ -47,14 +47,7 @@ extension NotificationViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
setupSegmentedControl(scopes: viewModel.scopes)
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false

View File

@ -15,8 +15,6 @@ import MastodonCore
import MastodonLocalization
final class MastodonServerRulesViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }

View File

@ -43,14 +43,7 @@ extension ProfileAboutViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.systemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .systemBackground
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)

View File

@ -44,14 +44,7 @@ extension BookmarkViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
navigationItem.titleView = titleView
titleView.update(title: L10n.Scene.Bookmark.title, subtitle: nil)

View File

@ -38,14 +38,7 @@ extension FamiliarFollowersViewController {
title = L10n.Scene.Familiarfollowers.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -47,14 +47,7 @@ extension FavoriteViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
navigationItem.titleView = titleView
titleView.update(title: L10n.Scene.Favorite.title, subtitle: nil)

View File

@ -43,14 +43,7 @@ extension FollowedTagsViewController {
navigationItem.titleView = titleView
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -42,14 +42,7 @@ extension FollowerListViewController {
title = L10n.Scene.Follower.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -42,14 +42,7 @@ extension FollowingListViewController {
title = L10n.Scene.Following.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -91,14 +91,7 @@ extension ProfileHeaderViewController {
view.setContentHuggingPriority(.required - 1, for: .vertical)
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.systemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .systemBackground
// profileHeaderView.preservesSuperviewLayoutMargins = true
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false

View File

@ -252,15 +252,7 @@ final class ProfileHeaderView: UIView {
extension ProfileHeaderView {
private func _init() {
let currentTheme = ThemeService.shared.currentTheme
setColors(from: currentTheme.value)
currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
self?.setColors(from: theme)
}
.store(in: &_disposeBag)
setColors()
// banner
bannerContainerView.translatesAutoresizingMaskIntoConstraints = false
@ -469,10 +461,10 @@ extension ProfileHeaderView {
updateLayoutMargins()
}
private func setColors(from theme: Theme) {
backgroundColor = theme.systemBackgroundColor
avatarButton.backgroundColor = theme.secondarySystemBackgroundColor
avatarImageViewBackgroundView.layer.borderColor = theme.systemBackgroundColor.cgColor
private func setColors() {
backgroundColor = .systemBackground
avatarButton.backgroundColor = .secondarySystemBackground
avatarImageViewBackgroundView.layer.borderColor = UIColor.systemBackground.cgColor
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {

View File

@ -64,7 +64,7 @@ extension ProfilePagingViewController {
override func viewDidLoad() {
// configure style before viewDidLoad
settings.style.buttonBarBackgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
settings.style.buttonBarBackgroundColor = .systemBackground
settings.style.buttonBarItemBackgroundColor = .clear
settings.style.buttonBarItemsShouldFillAvailableWidth = false // alignment from leading to trailing
settings.style.selectedBarHeight = 3
@ -80,21 +80,12 @@ extension ProfilePagingViewController {
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.buttonBarView.backgroundColor = self.settings.style.buttonBarBackgroundColor
self.barButtonLayout?.invalidateLayout()
}
.store(in: &disposeBag)
updateBarButtonInsets()
if let buttonBarView = self.buttonBarView {
buttonBarShadowView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(buttonBarShadowView, belowSubview: buttonBarView)
buttonBarView.backgroundColor = .systemBackground
buttonBarShadowView.pinTo(to: buttonBarView)
viewModel.$needsSetupBottomShadow

View File

@ -168,15 +168,7 @@ extension ProfileViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
let barAppearance = UINavigationBarAppearance()
if isModal {
barAppearance.configureWithDefaultBackground()
@ -527,8 +519,8 @@ extension ProfileViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, authContext: viewModel.authContext, setting: setting)
_ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}
@objc private func shareBarButtonItemPressed(_ sender: UIBarButtonItem) {

View File

@ -43,14 +43,7 @@ extension UserTimelineViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -38,14 +38,7 @@ extension FavoritedByViewController {
title = L10n.Scene.FavoritedBy.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -44,14 +44,7 @@ extension RebloggedByViewController {
title = L10n.Scene.RebloggedBy.title
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -167,14 +167,7 @@ extension MainTabBarController {
delegate = self
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.tabBarBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .systemBackground
// seealso: `ThemeService.apply(theme:)`
let tabs = Tab.allCases
@ -221,32 +214,7 @@ extension MainTabBarController {
.store(in: &disposeBag)
// handle post failure
// FIXME: refacotr
// context.statusPublishService
// .latestPublishingComposeViewModel
// .receive(on: DispatchQueue.main)
// .sink { [weak self] composeViewModel in
// guard let self = self else { return }
// guard let composeViewModel = composeViewModel else { return }
// guard let currentState = composeViewModel.publishStateMachine.currentState else { return }
// guard currentState is ComposeViewModel.PublishState.Fail else { return }
//
// let alertController = UIAlertController(title: L10n.Common.Alerts.PublishPostFailure.title, message: L10n.Common.Alerts.PublishPostFailure.message, preferredStyle: .alert)
// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self, weak composeViewModel] _ in
// guard let self = self else { return }
// guard let composeViewModel = composeViewModel else { return }
// self.context.statusPublishService.remove(composeViewModel: composeViewModel)
// }
// alertController.addAction(discardAction)
// let retryAction = UIAlertAction(title: L10n.Common.Controls.Actions.tryAgain, style: .default) { [weak composeViewModel] _ in
// guard let composeViewModel = composeViewModel else { return }
// composeViewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self)
// }
// alertController.addAction(retryAction)
// self.present(alertController, animated: true, completion: nil)
// }
// .store(in: &disposeBag)
// handle push notification.
// toggle entry when finish fetch latest notification
Publishers.CombineLatest(
@ -690,10 +658,9 @@ extension MainTabBarController {
}
@objc private func openSettingsKeyCommandHandler(_ sender: UIKeyCommand) {
guard let authContext = self.authContext else { return }
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, authContext: authContext, setting: setting)
_ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: nil, transition: .modal(animated: true, completion: nil))
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
}
@objc private func composeNewPostKeyCommandHandler(_ sender: UIKeyCommand) {

View File

@ -87,14 +87,7 @@ extension RootSplitViewController {
updateBehavior(size: view.frame.size)
setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackground(theme: theme)
}
.store(in: &disposeBag)
view.backgroundColor = .separator
}
override func viewWillAppear(_ animated: Bool) {
@ -112,13 +105,7 @@ extension RootSplitViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.portraitOnPhone
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setupBackground(theme: ThemeService.shared.currentTheme.value)
}
private func updateBehavior(size: CGSize) {
if size.width > 960 {
show(.primary)
@ -142,15 +129,6 @@ extension RootSplitViewController {
}
extension RootSplitViewController {
private func setupBackground(theme: Theme) {
// this set column separator line color
view.backgroundColor = theme.separator
}
}
// MARK: - ContentSplitViewControllerDelegate
extension RootSplitViewController: ContentSplitViewControllerDelegate {
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {

View File

@ -11,27 +11,11 @@ import MastodonCore
final class SecondaryPlaceholderViewController: UIViewController {
var disposeBag = Set<AnyCancellable>()
}
extension SecondaryPlaceholderViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackground(theme: theme)
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
}
}
extension SecondaryPlaceholderViewController {
private func setupBackground(theme: Theme) {
view.backgroundColor = theme.secondarySystemBackgroundColor
}
}

View File

@ -87,14 +87,7 @@ extension SidebarViewController {
navigationController?.setNavigationBarHidden(true, animated: false)
setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackground(theme: theme)
}
.store(in: &disposeBag)
view.backgroundColor = SystemTheme.sidebarBackgroundColor
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
@ -145,11 +138,6 @@ extension SidebarViewController {
}
private func setupBackground(theme: Theme) {
let color: UIColor = theme.sidebarBackgroundColor
view.backgroundColor = color
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
@ -203,10 +191,9 @@ extension SidebarViewController: UICollectionViewDelegate {
case .tab(let tab):
delegate?.sidebarViewController(self, didSelectTab: tab)
case .setting:
guard let authContext = viewModel.authContext else { return }
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, authContext: authContext, setting: setting)
_ = coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
_ = coordinator.present(scene: .settings(setting: setting), from: self, transition: .none)
case .compose:
assertionFailure()
}

View File

@ -92,7 +92,7 @@ extension SidebarListContentView {
guard let item = configuration.item else { return }
// configure state
let tintColor = item.isHighlighted ? ThemeService.tintColor.withAlphaComponent(0.5) : ThemeService.tintColor
let tintColor = item.isHighlighted ? SystemTheme.tintColor.withAlphaComponent(0.5) : SystemTheme.tintColor
imageView.tintColor = tintColor
avatarButton.tintColor = tintColor

View File

@ -37,15 +37,6 @@ final class TrendCollectionViewCell: UICollectionViewCell {
extension TrendCollectionViewCell {
private func _init() {
ThemeService.shared.currentTheme
.map { $0.secondarySystemGroupedBackgroundColor }
.sink { [weak self] backgroundColor in
guard let self = self else { return }
self.backgroundColor = backgroundColor
self.setNeedsUpdateConfiguration()
}
.store(in: &_disposeBag)
trendView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(trendView)
trendView.pinToParent()
@ -57,9 +48,9 @@ extension TrendCollectionViewCell {
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = .init { _ in
if state.isHighlighted || state.isSelected {
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
return SystemTheme.tableViewCellSelectionBackgroundColor
}
return ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
return .secondarySystemGroupedBackground
}
self.backgroundConfiguration = backgroundConfiguration
}

View File

@ -54,14 +54,7 @@ extension SearchViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupAppearance(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupAppearance(theme: theme)
}
.store(in: &disposeBag)
setupAppearance()
title = L10n.Scene.Search.title
@ -86,13 +79,13 @@ extension SearchViewController {
}
extension SearchViewController {
private func setupAppearance(theme: Theme) {
view.backgroundColor = theme.systemGroupedBackgroundColor
private func setupAppearance() {
view.backgroundColor = .systemGroupedBackground
// Match the DiscoveryViewController tab color and remove the double separator.
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithOpaqueBackground()
navigationBarAppearance.backgroundColor = theme.systemBackgroundColor
navigationBarAppearance.backgroundColor = .systemBackground
navigationBarAppearance.shadowColor = nil
navigationItem.standardAppearance = navigationBarAppearance

View File

@ -5,10 +5,6 @@ import MastodonCore
import MastodonSDK
import MastodonLocalization
protocol Coordinator {
func start()
}
class SearchResultOverviewCoordinator: Coordinator {
let overviewViewController: SearchResultsOverviewTableViewController

View File

@ -106,14 +106,7 @@ final class SearchDetailViewController: UIViewController, NeedsDependency {
super.viewDidLoad()
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
setupBackgroundColor()
setupSearchBar()
@ -246,8 +239,8 @@ extension SearchDetailViewController {
searchBar.delegate = self
}
private func setupBackgroundColor(theme: Theme) {
navigationBarBackgroundView.backgroundColor = theme.navigationBarBackgroundColor
private func setupBackgroundColor() {
navigationBarBackgroundView.backgroundColor = SystemTheme.navigationBarBackgroundColor
navigationBar.tintColor = Asset.Colors.Brand.blurple.color
}
}

View File

@ -32,7 +32,7 @@ class SearchHistoryUserCollectionViewCell: UICollectionViewCell {
var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = .init { _ in
if state.isHighlighted || state.isSelected {
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
return SystemTheme.tableViewCellSelectionBackgroundColor
} else {
return .secondarySystemGroupedBackground
}

View File

@ -52,7 +52,7 @@ extension SearchHistorySection {
}
if state.isHighlighted || state.isSelected {
return ThemeService.shared.currentTheme.value.tableViewCellSelectionBackgroundColor
return SystemTheme.tableViewCellSelectionBackgroundColor
}
return .secondarySystemGroupedBackground
}

View File

@ -36,15 +36,7 @@ extension SearchHistoryViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
view.backgroundColor = .systemGroupedBackground
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
collectionView.pinToParent()
@ -57,12 +49,6 @@ extension SearchHistoryViewController {
}
}
extension SearchHistoryViewController {
private func setupBackgroundColor(theme: Theme) {
view.backgroundColor = theme.systemGroupedBackgroundColor
}
}
// MARK: - UICollectionViewDelegate
extension SearchHistoryViewController: UICollectionViewDelegate {

View File

@ -37,7 +37,7 @@ extension SearchResultViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Asset.Theme.System.systemGroupedBackground.color
view.backgroundColor = .systemGroupedBackground
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

View File

@ -0,0 +1,18 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
class AboutMastodonTableViewCell: UITableViewCell {
static let reuseIdentifier = "AboutMastodonTableViewCell"
func configure(with entry: AboutSettingsEntry) {
var contentConfiguration = UIListContentConfiguration.valueCell()
contentConfiguration.text = entry.text
contentConfiguration.secondaryText = entry.secondaryText
contentConfiguration.textProperties.color = Asset.Colors.Brand.blurple.color
self.contentConfiguration = contentConfiguration
}
}

View File

@ -0,0 +1,38 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonCore
import MastodonLocalization
struct AboutSettingsSection: Hashable {
let entries: [AboutSettingsEntry]
}
enum AboutSettingsEntry: Hashable {
case evenMoreSettings
case contributeToMastodon
case privacyPolicy
case clearMediaCache(Int)
var text: String {
switch self {
case .evenMoreSettings:
return L10n.Scene.Settings.AboutMastodon.moreSettings
case .contributeToMastodon:
return L10n.Scene.Settings.AboutMastodon.contributeToMastodon
case .privacyPolicy:
return L10n.Scene.Settings.AboutMastodon.privacyPolicy
case .clearMediaCache(_):
return L10n.Scene.Settings.AboutMastodon.cleaerMediaStorage
}
}
var secondaryText: String? {
switch self {
case .evenMoreSettings, .contributeToMastodon, .privacyPolicy:
return nil
case .clearMediaCache(let mediaStorage):
return AppContext.byteCountFormatter.string(fromByteCount: Int64(mediaStorage))
}
}
}

View File

@ -0,0 +1,136 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonCore
import MastodonLocalization
import MastodonAsset
protocol AboutViewControllerDelegate: AnyObject {
func didSelect(_ viewController: AboutViewController, entry: AboutSettingsEntry)
}
class AboutViewController: UIViewController {
let tableView: UITableView
let tableFooterView: UIView
let versionLabel: UILabel
private(set) var sections: [AboutSettingsSection] = []
var tableViewDataSource: UITableViewDiffableDataSource<AboutSettingsSection, AboutSettingsEntry>?
weak var delegate: AboutViewControllerDelegate?
init() {
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(AboutMastodonTableViewCell.self, forCellReuseIdentifier: AboutMastodonTableViewCell.reuseIdentifier)
versionLabel = UILabel()
versionLabel.translatesAutoresizingMaskIntoConstraints = false
versionLabel.text = "Mastodon for iOS v\(UIApplication.appVersion()) (\(UIApplication.appBuild()))"
versionLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 12, weight: .regular))
versionLabel.textColor = Asset.Colors.Label.secondary.color
versionLabel.numberOfLines = 0
versionLabel.textAlignment = .center
tableFooterView = UIView()
tableFooterView.addSubview(versionLabel)
super.init(nibName: nil, bundle: nil)
let tableViewDataSource = UITableViewDiffableDataSource<AboutSettingsSection, AboutSettingsEntry>(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in
guard let self,
let cell = tableView.dequeueReusableCell(withIdentifier: AboutMastodonTableViewCell.reuseIdentifier, for: indexPath) as? AboutMastodonTableViewCell else { fatalError("WTF?? Wrong Cell dude!") }
let entry = self.sections[indexPath.section].entries[indexPath.row]
cell.configure(with: entry)
return cell
}
tableView.delegate = self
tableView.dataSource = tableViewDataSource
tableView.tableFooterView = tableFooterView
self.tableViewDataSource = tableViewDataSource
view.addSubview(tableView)
view.backgroundColor = .systemGroupedBackground
title = L10n.Scene.Settings.AboutMastodon.title
setupConstraints()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
update(with:
[AboutSettingsSection(entries: [
.evenMoreSettings,
.contributeToMastodon,
.privacyPolicy
]),
AboutSettingsSection(entries: [
.clearMediaCache(AppContext.shared.currentDiskUsage())
])]
)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let footerView = self.tableView.tableFooterView else {
return
}
let width = self.tableView.bounds.size.width
let size = footerView.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height))
if footerView.frame.size.height != size.height {
footerView.frame.size.height = size.height
self.tableView.tableFooterView = footerView
}
}
private func setupConstraints() {
let constraints = [
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
view.trailingAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.trailingAnchor),
view.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor),
versionLabel.topAnchor.constraint(equalTo: tableFooterView.topAnchor, constant: 8),
versionLabel.leadingAnchor.constraint(equalTo: tableFooterView.leadingAnchor),
tableFooterView.trailingAnchor.constraint(equalTo: versionLabel.trailingAnchor),
tableFooterView.bottomAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 16),
]
NSLayoutConstraint.activate(constraints)
}
func update(with sections: [AboutSettingsSection]) {
self.sections = sections
var snapshot = NSDiffableDataSourceSnapshot<AboutSettingsSection, AboutSettingsEntry>()
for section in sections {
snapshot.appendSections([section])
snapshot.appendItems(section.entries)
}
tableViewDataSource?.apply(snapshot, animatingDifferences: false)
}
}
extension AboutViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = sections[indexPath.section].entries[indexPath.row]
delegate?.didSelect(self, entry: entry)
tableView.deselectRow(at: indexPath, animated: true)
}
}

View File

@ -1,59 +0,0 @@
//
// SettingsAppearanceTableViewCell+ViewModel.swift
// Mastodon
//
// Created by MainasuK on 2022-2-8.
//
import UIKit
import Combine
import CoreDataStack
extension SettingsAppearanceTableViewCell {
final class ViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>()
private var observations = Set<NSKeyValueObservation>()
// input
@Published public var customUserInterfaceStyle: UIUserInterfaceStyle = .unspecified
// output
@Published public var appearanceMode: SettingsItem.AppearanceMode = .system
init() {
UserDefaults.shared.observe(\.customUserInterfaceStyle, options: [.initial, .new]) { [weak self] defaults, _ in
guard let self = self else { return }
self.customUserInterfaceStyle = defaults.customUserInterfaceStyle
}
.store(in: &observations)
}
public func prepareForReuse() {
// do nothing
}
}
}
extension SettingsAppearanceTableViewCell.ViewModel {
func bind(cell: SettingsAppearanceTableViewCell) {
$customUserInterfaceStyle.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { customUserInterfaceStyle in
cell.appearanceViews.forEach { view in
view.selected = false
}
switch customUserInterfaceStyle {
case .unspecified:
cell.systemAppearanceView.selected = true
case .dark:
cell.darkAppearanceView.selected = true
case .light:
cell.lightAppearanceView.selected = true
@unknown default:
assertionFailure()
}
}
.store(in: &disposeBag)
}
}

View File

@ -1,135 +0,0 @@
//
// SettingsAppearanceTableViewCell.swift
// Mastodon
//
// Created by ihugo on 2021/4/8.
//
import UIKit
import Combine
import MastodonAsset
import MastodonLocalization
protocol SettingsAppearanceTableViewCellDelegate: AnyObject {
func settingsAppearanceTableViewCell(_ cell: SettingsAppearanceTableViewCell, didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode)
}
class SettingsAppearanceTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
static let spacing: CGFloat = 28
weak var delegate: SettingsAppearanceTableViewCellDelegate?
public private(set) var viewModel = ViewModel()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.distribution = .fillEqually
view.spacing = SettingsAppearanceTableViewCell.spacing
return view
}()
let systemAppearanceView = AppearanceView(
image: Asset.Settings.automatic.image,
title: L10n.Scene.Settings.Section.Appearance.automatic
)
let darkAppearanceView = AppearanceView(
image: Asset.Settings.dark.image,
title: L10n.Scene.Settings.Section.Appearance.dark
)
let lightAppearanceView = AppearanceView(
image: Asset.Settings.light.image,
title: L10n.Scene.Settings.Section.Appearance.light
)
var appearanceViews: [AppearanceView] {
return [
systemAppearanceView,
darkAppearanceView,
lightAppearanceView,
]
}
override func prepareForReuse() {
super.prepareForReuse()
disposeBag.removeAll()
observations.removeAll()
viewModel.prepareForReuse()
}
// MARK: - Methods
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
viewModel.bind(cell: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
// remove separator line in section of group tableview
for subview in self.subviews {
if subview != self.contentView && subview.frame.width == self.frame.width {
subview.removeFromSuperview()
}
}
// remove grouped style table corner radius
layer.cornerRadius = 0
}
}
extension SettingsAppearanceTableViewCell {
// MARK: Private methods
private func setupUI() {
backgroundColor = .clear
selectionStyle = .none
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
stackView.pinToParent()
stackView.addArrangedSubview(systemAppearanceView)
stackView.addArrangedSubview(darkAppearanceView)
stackView.addArrangedSubview(lightAppearanceView)
appearanceViews.forEach { view in
let tapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
view.addGestureRecognizer(tapGestureRecognizer)
tapGestureRecognizer.addTarget(self, action: #selector(SettingsAppearanceTableViewCell.appearanceViewDidPressed(_:)))
}
}
}
// MARK: - Actions
extension SettingsAppearanceTableViewCell {
@objc func appearanceViewDidPressed(_ sender: UITapGestureRecognizer) {
let mode: SettingsItem.AppearanceMode
switch sender.view {
case systemAppearanceView:
mode = .system
case darkAppearanceView:
mode = .dark
case lightAppearanceView:
mode = .light
default:
assertionFailure()
return
}
delegate?.settingsAppearanceTableViewCell(self, didSelectAppearanceMode: mode)
}
}

View File

@ -1,35 +0,0 @@
//
// SettingsLinkTableViewCell.swift
// Mastodon
//
// Created by ihugo on 2021/4/8.
//
import UIKit
class SettingsLinkTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
textLabel?.alpha = highlighted ? 0.6 : 1.0
}
}
// MARK: - Methods
extension SettingsLinkTableViewCell {
func update(with link: SettingsItem.Link) {
textLabel?.text = link.title
textLabel?.textColor = link.textColor
}
}

View File

@ -1,75 +0,0 @@
//
// SettingsToggleTableViewCell.swift
// Mastodon
//
// Created by ihugo on 2021/4/8.
//
import UIKit
import Combine
import MastodonAsset
import MastodonLocalization
protocol SettingsToggleCellDelegate: AnyObject {
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch)
}
class SettingsToggleTableViewCell: UITableViewCell {
var disposeBag = Set<AnyCancellable>()
private(set) lazy var switchButton: UISwitch = {
let view = UISwitch(frame:.zero)
view.onTintColor = Asset.Colors.Brand.blurple.color
return view
}()
weak var delegate: SettingsToggleCellDelegate?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag.removeAll()
}
// MARK: - Methods
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
// MARK: Private methods
private func setupUI() {
selectionStyle = .none
accessoryView = switchButton
textLabel?.numberOfLines = 0
switchButton.addTarget(self, action: #selector(switchValueDidChange(sender:)), for: .valueChanged)
}
}
// MARK: - Actions
extension SettingsToggleTableViewCell {
@objc private func switchValueDidChange(sender: UISwitch) {
guard let delegate = delegate else { return }
delegate.settingsToggleCell(self, switchValueDidChange: sender)
}
}
extension SettingsToggleTableViewCell {
func update(enabled: Bool?) {
switchButton.isEnabled = enabled != nil
textLabel?.textColor = enabled != nil ? Asset.Colors.Label.primary.color : Asset.Colors.Label.secondary.color
switchButton.isOn = enabled ?? false
}
}

View File

@ -0,0 +1,49 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
class GeneralSettingSelectionCell: UITableViewCell {
static let reuseIdentifier = "GeneralSettingSelectionCell"
func configure(with setting: GeneralSetting, viewModel: GeneralSettingsViewModel) {
switch setting {
case .appearance(let appearanceSetting):
configureAppearanceSetting(appearanceSetting: appearanceSetting, viewModel: viewModel)
case .design(_):
// only for appearance and open links
assertionFailure("Wrong Setting!")
case .openLinksIn(let openLinkSetting):
configureOpenLinkSetting(openLinkSetting: openLinkSetting, viewModel: viewModel)
}
}
private func configureAppearanceSetting(appearanceSetting: GeneralSetting.Appearance, viewModel: GeneralSettingsViewModel) {
var content = defaultContentConfiguration()
content.text = appearanceSetting.title
tintColor = Asset.Colors.Brand.blurple.color
if viewModel.selectedAppearence == appearanceSetting {
accessoryType = .checkmark
} else {
accessoryType = .none
}
contentConfiguration = content
}
private func configureOpenLinkSetting(openLinkSetting: GeneralSetting.OpenLinksIn, viewModel: GeneralSettingsViewModel) {
var content = defaultContentConfiguration()
content.text = openLinkSetting.title
tintColor = Asset.Colors.Brand.blurple.color
if viewModel.selectedOpenLinks == openLinkSetting {
accessoryType = .checkmark
} else {
accessoryType = .none
}
contentConfiguration = content
}
}

View File

@ -0,0 +1,48 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
protocol GeneralSettingToggleTableViewCellDelegate: AnyObject {
func toggle(_ cell: GeneralSettingToggleTableViewCell, setting: GeneralSetting, isOn: Bool)
}
class GeneralSettingToggleTableViewCell: ToggleTableViewCell {
override class var reuseIdentifier: String {
return "GeneralSettingToggleCell"
}
weak var delegate: GeneralSettingToggleTableViewCellDelegate?
var setting: GeneralSetting?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
toggle.addTarget(self, action: #selector(GeneralSettingToggleTableViewCell.toggleValueChanged(_:)), for: .valueChanged)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(with setting: GeneralSetting, viewModel: GeneralSettingsViewModel) {
self.setting = setting
switch setting {
case .appearance(_), .openLinksIn(_):
assertionFailure("Only for Design")
case .design(let designSetting):
label.text = designSetting.title
switch designSetting {
case .showAnimations:
toggle.isOn = viewModel.playAnimations
}
}
}
@objc
func toggleValueChanged(_ sender: UISwitch) {
guard let setting else { return }
delegate?.toggle(self, setting: setting, isOn: sender.isOn)
}
}

View File

@ -0,0 +1,79 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
struct GeneralSettingsSection: Hashable {
let type: GeneralSettingsSectionType
let entries: [GeneralSetting]
}
enum GeneralSettingsSectionType: Hashable {
case appearance
case design
case links
var sectionTitle: String {
switch self {
case .appearance:
return L10n.Scene.Settings.General.Appearance.sectionTitle
case .design:
return L10n.Scene.Settings.General.Design.sectionTitle
case .links:
return L10n.Scene.Settings.General.Links.sectionTitle
}
}
}
enum GeneralSetting: Hashable {
case appearance(Appearance)
case design(Design)
case openLinksIn(OpenLinksIn)
enum Appearance: Int, CaseIterable {
case light = 1
case dark = 2
case system = 0
var title: String {
switch self {
case .light:
return L10n.Scene.Settings.General.Appearance.light
case .dark:
return L10n.Scene.Settings.General.Appearance.dark
case .system:
return L10n.Scene.Settings.General.Appearance.system
}
}
var interfaceStyle: UIUserInterfaceStyle {
.init(rawValue: rawValue) ?? .unspecified
}
}
enum Design: Hashable {
case showAnimations
var title: String {
switch self {
case .showAnimations:
return L10n.Scene.Settings.General.Design.showAnimations
}
}
}
enum OpenLinksIn: Hashable, CaseIterable {
case mastodon
case browser
var title: String {
switch self {
case .mastodon:
return L10n.Scene.Settings.General.Links.openInMastodon
case .browser:
return L10n.Scene.Settings.General.Links.openInBrowser
}
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
class GeneralSettingsDiffableTableViewDataSource: UITableViewDiffableDataSource<GeneralSettingsSection, GeneralSetting> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let settingsSection = sectionIdentifier(for: section) else { return nil }
return settingsSection.type.sectionTitle.uppercased()
}
}

View File

@ -0,0 +1,169 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonSDK
import CoreDataStack
import MastodonLocalization
struct GeneralSettingsViewModel {
var selectedAppearence: GeneralSetting.Appearance
var playAnimations: Bool
var selectedOpenLinks: GeneralSetting.OpenLinksIn
}
protocol GeneralSettingsViewControllerDelegate: AnyObject {
func save(_ viewController: UIViewController, setting: Setting, viewModel: GeneralSettingsViewModel)
}
class GeneralSettingsViewController: UIViewController {
weak var delegate: GeneralSettingsViewControllerDelegate?
let tableView: UITableView
var tableViewDataSource: GeneralSettingsDiffableTableViewDataSource?
private(set) var viewModel: GeneralSettingsViewModel
let setting: Setting
let sections: [GeneralSettingsSection]
init(setting: Setting) {
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(GeneralSettingSelectionCell.self, forCellReuseIdentifier: GeneralSettingSelectionCell.reuseIdentifier)
tableView.register(GeneralSettingToggleTableViewCell.self, forCellReuseIdentifier: GeneralSettingToggleTableViewCell.reuseIdentifier)
sections = [
GeneralSettingsSection(type: .appearance, entries: [
.appearance(.light),
.appearance(.dark),
.appearance(.system)
]),
GeneralSettingsSection(type: .design, entries: [
.design(.showAnimations)
]),
GeneralSettingsSection(type: .links, entries: [
.openLinksIn(.mastodon),
.openLinksIn(.browser),
])
]
let openLinksIn: GeneralSetting.OpenLinksIn
if UserDefaults.shared.preferredUsingDefaultBrowser {
openLinksIn = .browser
} else {
openLinksIn = .mastodon
}
let playAnimations = (UserDefaults.shared.preferredStaticAvatar == false && UserDefaults.shared.preferredStaticEmoji == false)
viewModel = GeneralSettingsViewModel(
selectedAppearence: GeneralSetting.Appearance(rawValue: UserDefaults.shared.customUserInterfaceStyle.rawValue) ?? .system,
playAnimations: playAnimations,
selectedOpenLinks: openLinksIn
)
self.setting = setting
super.init(nibName: nil, bundle: nil)
tableView.delegate = self
let tableViewDataSource = GeneralSettingsDiffableTableViewDataSource(tableView: tableView, cellProvider: { tableView, indexPath, itemIdentifier in
let cell: UITableViewCell
switch itemIdentifier {
case .appearance(let setting):
guard let selectionCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingSelectionCell.reuseIdentifier, for: indexPath) as? GeneralSettingSelectionCell else { fatalError("WTF? Wrong Cell!") }
selectionCell.configure(with: .appearance(setting), viewModel: self.viewModel)
cell = selectionCell
case .design(let setting):
guard let toggleCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingToggleTableViewCell.reuseIdentifier, for: indexPath) as? GeneralSettingToggleTableViewCell else { fatalError("WTF? Wrong Cell!") }
toggleCell.configure(with: .design(setting), viewModel: self.viewModel)
toggleCell.delegate = self
cell = toggleCell
case .openLinksIn(let setting):
guard let selectionCell = tableView.dequeueReusableCell(withIdentifier: GeneralSettingSelectionCell.reuseIdentifier, for: indexPath) as? GeneralSettingSelectionCell else { fatalError("WTF? Wrong Cell!") }
selectionCell.configure(with: .openLinksIn(setting), viewModel: self.viewModel)
cell = selectionCell
}
return cell
})
self.tableViewDataSource = tableViewDataSource
view.backgroundColor = .systemGroupedBackground
view.addSubview(tableView)
tableView.pinTo(to: view)
title = L10n.Scene.Settings.General.title
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = NSDiffableDataSourceSnapshot<GeneralSettingsSection, GeneralSetting>()
for section in sections {
snapshot.appendSections([section])
snapshot.appendItems(section.entries)
}
tableViewDataSource?.apply(snapshot, animatingDifferences: false)
}
}
extension GeneralSettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// switch section
let section = sections[indexPath.section].entries[indexPath.row]
switch section {
case .appearance(let appearanceOption):
viewModel.selectedAppearence = appearanceOption
if let snapshot = tableViewDataSource?.snapshot() {
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
}
case .design(let design):
guard let cell = tableView.cellForRow(at: indexPath) as? GeneralSettingToggleTableViewCell else { return}
let newValue = (cell.toggle.isOn == false)
cell.toggle.setOn(newValue, animated: true)
toggle(cell, setting: .design(design), isOn: newValue)
case .openLinksIn(let openLinksInOption):
viewModel.selectedOpenLinks = openLinksInOption
if let snapshot = tableViewDataSource?.snapshot() {
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
}
}
tableView.deselectRow(at: indexPath, animated: true)
delegate?.save(self, setting: setting, viewModel: viewModel)
}
}
extension GeneralSettingsViewController: GeneralSettingToggleTableViewCellDelegate {
func toggle(_ cell: GeneralSettingToggleTableViewCell, setting: GeneralSetting, isOn: Bool) {
switch setting {
case .appearance(_), .openLinksIn(_):
assertionFailure("No toggle")
case .design(let designSetting):
switch designSetting {
case .showAnimations:
viewModel.playAnimations = isOn
}
}
delegate?.save(self, setting: self.setting, viewModel: viewModel)
}
}

View File

@ -0,0 +1,29 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
class NotificationSettingTableViewCell: UITableViewCell {
static let reuseIdentifier = "NotificationSettingTableViewCell"
func configure(with entry: NotificationSettingEntry, viewModel: NotificationSettingsViewModel, notificationsEnabled: Bool) {
isUserInteractionEnabled = notificationsEnabled
guard case .policy = entry else { return }
var content = UIListContentConfiguration.valueCell()
content.text = L10n.Scene.Settings.Notifications.Policy.title
content.secondaryText = viewModel.selectedPolicy.title
if notificationsEnabled {
content.textProperties.color = .label
content.secondaryTextProperties.color = .secondaryLabel
} else {
content.textProperties.color = .secondaryLabel
content.secondaryTextProperties.color = .tertiaryLabel
}
contentConfiguration = content
}
}

View File

@ -0,0 +1,60 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
protocol NotificationSettingToggleCellDelegate: AnyObject {
func toggleValueChanged(_ tableViewCell: NotificationSettingTableViewToggleCell, alert: NotificationAlert, newValue: Bool)
}
class NotificationSettingTableViewToggleCell: ToggleTableViewCell {
override class var reuseIdentifier: String {
return "NotificationSettingToggleCell"
}
var alert: NotificationAlert?
weak var delegate: NotificationSettingToggleCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
toggle.addTarget(self, action: #selector(NotificationSettingTableViewToggleCell.toggleValueChanged(_:)), for: .valueChanged)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(with alert: NotificationAlert, viewModel: NotificationSettingsViewModel, notificationsEnabled: Bool) {
isUserInteractionEnabled = notificationsEnabled
self.alert = alert
let toggleIsOn: Bool
switch alert {
case .mentionsAndReplies:
toggleIsOn = viewModel.notifyMentions
case .boosts:
toggleIsOn = viewModel.notifyBoosts
case .favorites:
toggleIsOn = viewModel.notifyFavorites
case .newFollowers:
toggleIsOn = viewModel.notifyNewFollowers
}
label.text = alert.title
if notificationsEnabled {
label.textColor = .label
} else {
label.textColor = .secondaryLabel
}
toggle.isOn = toggleIsOn && notificationsEnabled
toggle.isEnabled = notificationsEnabled
}
@objc
func toggleValueChanged(_ sender: UISwitch) {
guard let alert else { return }
delegate?.toggleValueChanged(self, alert: alert, newValue: sender.isOn)
}
}

View File

@ -0,0 +1,72 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
import MastodonLocalization
fileprivate extension CGFloat {
static let padding: Self = 16
static let appBadgeHeight: Self = 34
}
class NotificationSettingsDisabledTableViewCell: UITableViewCell {
static let reuseIdentifier = "NotificationSettingsDisabledTableViewCell"
let appBadgeImageView: UIImageView
let notificationHintLabel: UILabel
let goToSettingsLabel: UILabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
appBadgeImageView = UIImageView(image: UIImage(systemName: "app.badge.fill"))
appBadgeImageView.tintColor = Asset.Colors.Brand.blurple.color
appBadgeImageView.translatesAutoresizingMaskIntoConstraints = false
notificationHintLabel = UILabel()
notificationHintLabel.translatesAutoresizingMaskIntoConstraints = false
notificationHintLabel.numberOfLines = 0
notificationHintLabel.textColor = .label
notificationHintLabel.text = L10n.Scene.Settings.Notifications.Disabled.notificationHint
notificationHintLabel.font = UIFontMetrics(forTextStyle: .callout).scaledFont(for: .systemFont(ofSize: 16, weight: .regular))
goToSettingsLabel = UILabel()
goToSettingsLabel.textColor = Asset.Colors.Brand.blurple.color
goToSettingsLabel.translatesAutoresizingMaskIntoConstraints = false
goToSettingsLabel.text = L10n.Scene.Settings.Notifications.Disabled.goToSettings
goToSettingsLabel.font = UIFontMetrics(forTextStyle: .callout).scaledFont(for: .systemFont(ofSize: 16, weight: .bold))
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(appBadgeImageView)
contentView.addSubview(notificationHintLabel)
contentView.addSubview(goToSettingsLabel)
backgroundColor = Asset.Colors.Brand.blurple.color.withAlphaComponent(0.15)
setupConstraints()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupConstraints() {
let constraints: [NSLayoutConstraint] = [
appBadgeImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: .padding),
appBadgeImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: .padding),
appBadgeImageView.heightAnchor.constraint(equalToConstant: .appBadgeHeight),
appBadgeImageView.widthAnchor.constraint(equalTo: appBadgeImageView.heightAnchor),
notificationHintLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: .padding),
notificationHintLabel.leadingAnchor.constraint(equalTo: appBadgeImageView.trailingAnchor, constant: .padding),
contentView.trailingAnchor.constraint(equalTo: notificationHintLabel.trailingAnchor, constant: .padding),
goToSettingsLabel.topAnchor.constraint(equalTo: notificationHintLabel.bottomAnchor, constant: .padding/2),
goToSettingsLabel.leadingAnchor.constraint(equalTo: notificationHintLabel.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: goToSettingsLabel.trailingAnchor, constant: .padding),
contentView.bottomAnchor.constraint(equalTo: goToSettingsLabel.bottomAnchor, constant: .padding),
]
NSLayoutConstraint.activate(constraints)
}
}

View File

@ -0,0 +1,93 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
import MastodonLocalization
import MastodonSDK
import CoreDataStack
struct NotificationSettingsSection: Hashable {
let entries: [NotificationSettingEntry]
}
enum NotificationSettingEntry: Hashable {
case notificationDisabled
case policy
case alert(NotificationAlert)
}
struct NotificationPolicySection: Hashable {
let entries: [NotificationPolicy]
}
enum NotificationPolicy: Hashable, CaseIterable {
case anyone
case followers
case follow
case noone
var title: String {
switch self {
case .anyone:
return L10n.Scene.Settings.Notifications.Policy.anyone
case .followers:
return L10n.Scene.Settings.Notifications.Policy.followers
case .follow:
return L10n.Scene.Settings.Notifications.Policy.follow
case .noone:
return L10n.Scene.Settings.Notifications.Policy.noone
}
}
var subscriptionPolicy: Mastodon.API.Subscriptions.Policy {
switch self {
case .anyone:
return .all
case .followers:
return .follower
case .follow:
return .followed
case .noone:
return .none
}
}
}
enum NotificationAlert: Hashable, CaseIterable {
case mentionsAndReplies
case boosts
case favorites
case newFollowers
var title: String {
switch self {
case .mentionsAndReplies:
return L10n.Scene.Settings.Notifications.Alert.mentionsAndReplies
case .boosts:
return L10n.Scene.Settings.Notifications.Alert.boosts
case .favorites:
return L10n.Scene.Settings.Notifications.Alert.favorites
case .newFollowers:
return L10n.Scene.Settings.Notifications.Alert.newFollowers
}
}
}
extension Subscription {
var notificationPolicy: NotificationPolicy? {
guard let policy else { return nil }
switch policy {
case .all:
return .anyone
case .followed:
return .follow
case .follower:
return .followers
case .none:
return .noone
case ._other(_):
return .noone
}
}
}

View File

@ -0,0 +1,162 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import CoreDataStack
import MastodonLocalization
protocol NotificationSettingsViewControllerDelegate: AnyObject {
func viewWillDisappear(_ viewController: UIViewController, viewModel: NotificationSettingsViewModel)
func showPolicyList(_ viewController: UIViewController, viewModel: NotificationSettingsViewModel)
func showNotificationSettings(_ viewController: UIViewController)
}
class NotificationSettingsViewController: UIViewController {
weak var delegate: NotificationSettingsViewControllerDelegate?
let tableView: UITableView
var tableViewDataSource: UITableViewDiffableDataSource<NotificationSettingsSection, NotificationSettingEntry>?
let sections: [NotificationSettingsSection]
var viewModel: NotificationSettingsViewModel
init(currentSetting: Setting?, notificationsEnabled: Bool) {
let activeSubscription = currentSetting?.activeSubscription
let alert = activeSubscription?.alert
viewModel = NotificationSettingsViewModel(selectedPolicy: activeSubscription?.notificationPolicy ?? .noone,
notifyMentions: alert?.mention ?? false,
notifyBoosts: alert?.reblog ?? false,
notifyFavorites: alert?.favourite ?? false,
notifyNewFollowers: alert?.follow ?? false)
if notificationsEnabled {
sections = [
NotificationSettingsSection(entries: [.policy]),
NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } )
]
} else {
sections = [
NotificationSettingsSection(entries: [.notificationDisabled]),
NotificationSettingsSection(entries: [.policy]),
NotificationSettingsSection(entries: NotificationAlert.allCases.map { NotificationSettingEntry.alert($0) } )
]
}
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(NotificationSettingTableViewCell.self, forCellReuseIdentifier: NotificationSettingTableViewCell.reuseIdentifier)
tableView.register(NotificationSettingTableViewToggleCell.self, forCellReuseIdentifier: NotificationSettingTableViewToggleCell.reuseIdentifier)
tableView.register(NotificationSettingsDisabledTableViewCell.self, forCellReuseIdentifier: NotificationSettingsDisabledTableViewCell.reuseIdentifier)
super.init(nibName: nil, bundle: nil)
let tableViewDataSource = UITableViewDiffableDataSource<NotificationSettingsSection, NotificationSettingEntry>(tableView: tableView) { [ weak self] tableView, indexPath, itemIdentifier in
let cell: UITableViewCell
switch itemIdentifier {
case .notificationDisabled:
guard let notificationsDisabledCell = tableView.dequeueReusableCell(withIdentifier: NotificationSettingsDisabledTableViewCell.reuseIdentifier, for: indexPath) as? NotificationSettingsDisabledTableViewCell else { fatalError("WTF Wrong cell!?") }
cell = notificationsDisabledCell
case .policy:
guard let self,
let notificationCell = tableView.dequeueReusableCell(withIdentifier: NotificationSettingTableViewCell.reuseIdentifier, for: indexPath) as? NotificationSettingTableViewCell else { fatalError("WTF Wrong cell!?") }
notificationCell.configure(with: .policy, viewModel: self.viewModel, notificationsEnabled: notificationsEnabled)
cell = notificationCell
case .alert(let alert):
guard let self,
let toggleCell = tableView.dequeueReusableCell(withIdentifier: NotificationSettingTableViewToggleCell.reuseIdentifier, for: indexPath) as? NotificationSettingTableViewToggleCell else { fatalError("WTF Wrong cell!?") }
toggleCell.configure(with: alert, viewModel: self.viewModel, notificationsEnabled: notificationsEnabled)
toggleCell.delegate = self
cell = toggleCell
}
return cell
}
tableView.dataSource = tableViewDataSource
tableView.delegate = self
self.tableViewDataSource = tableViewDataSource
view.backgroundColor = .systemGroupedBackground
view.addSubview(tableView)
tableView.pinToParent()
title = L10n.Scene.Settings.Notifications.title
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = NSDiffableDataSourceSnapshot<NotificationSettingsSection, NotificationSettingEntry>()
for section in sections {
snapshot.appendSections([section])
snapshot.appendItems(section.entries)
}
tableViewDataSource?.apply(snapshot, animatingDifferences: false)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let snapshot = tableViewDataSource?.snapshot() {
tableViewDataSource?.applySnapshotUsingReloadData(snapshot)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.viewWillDisappear(self, viewModel: viewModel)
}
}
extension NotificationSettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = sections[indexPath.section].entries[indexPath.row]
switch entry {
case .alert(let alert):
guard let cell = tableView.cellForRow(at: indexPath) as? NotificationSettingTableViewToggleCell else { return }
let newValue = (cell.toggle.isOn == false)
cell.toggle.setOn(newValue, animated: true)
toggleValueChanged(cell, alert: alert, newValue: newValue)
case .policy:
delegate?.showPolicyList(self, viewModel: viewModel)
case .notificationDisabled:
delegate?.showNotificationSettings(self)
}
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension NotificationSettingsViewController: NotificationSettingToggleCellDelegate {
func toggleValueChanged(_ tableViewCell: NotificationSettingTableViewToggleCell, alert: NotificationAlert, newValue: Bool) {
switch alert {
case .mentionsAndReplies:
viewModel.notifyMentions = newValue
case .boosts:
viewModel.notifyBoosts = newValue
case .favorites:
viewModel.notifyFavorites = newValue
case .newFollowers:
viewModel.notifyNewFollowers = newValue
}
viewModel.updated = true
}
}

View File

@ -0,0 +1,24 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import Foundation
class NotificationSettingsViewModel {
var selectedPolicy: NotificationPolicy
var notifyMentions: Bool
var notifyBoosts: Bool
var notifyFavorites: Bool
var notifyNewFollowers: Bool
var updated: Bool
init(selectedPolicy: NotificationPolicy, notifyMentions: Bool, notifyBoosts: Bool, notifyFavorites: Bool, notifyNewFollowers: Bool) {
self.selectedPolicy = selectedPolicy
self.notifyMentions = notifyMentions
self.notifyBoosts = notifyBoosts
self.notifyFavorites = notifyFavorites
self.notifyNewFollowers = notifyNewFollowers
self.updated = false
}
}

View File

@ -0,0 +1,22 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
class NotificationPolicyTableViewCell: UITableViewCell {
static let reuseIdentifier = "NotificationPolicyTableViewCell"
func configure(with policy: NotificationPolicy, selectedPolicy: NotificationPolicy) {
var content = UIListContentConfiguration.cell()
content.text = policy.title
tintColor = Asset.Colors.Brand.blurple.color
if policy == selectedPolicy {
accessoryType = .checkmark
} else {
accessoryType = .none
}
contentConfiguration = content
}
}

View File

@ -0,0 +1,80 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
protocol PolicySelectionViewControllerDelegate: AnyObject {
func newPolicySelected(_ viewController: PolicySelectionViewController, newPolicy: NotificationPolicy)
}
class PolicySelectionViewController: UIViewController {
weak var delegate: PolicySelectionViewControllerDelegate?
let tableView: UITableView
var dataSource: UITableViewDiffableDataSource<NotificationPolicySection, NotificationPolicy>?
var viewModel: NotificationSettingsViewModel
let sections = [NotificationPolicySection(entries: NotificationPolicy.allCases)]
init(viewModel: NotificationSettingsViewModel) {
self.viewModel = viewModel
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(NotificationPolicyTableViewCell.self, forCellReuseIdentifier: NotificationPolicyTableViewCell.reuseIdentifier)
super.init(nibName: nil, bundle: nil)
let dataSource = UITableViewDiffableDataSource<NotificationPolicySection, NotificationPolicy>(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in
guard let self, let cell = tableView.dequeueReusableCell(withIdentifier: NotificationPolicyTableViewCell.reuseIdentifier, for: indexPath) as? NotificationPolicyTableViewCell else {
fatalError("WTF Wrong cell?!")
}
let policy = self.sections[indexPath.section].entries[indexPath.row]
cell.configure(with: policy, selectedPolicy: self.viewModel.selectedPolicy)
return cell
}
view.addSubview(tableView)
view.backgroundColor = .systemGroupedBackground
tableView.pinToParent()
tableView.delegate = self
tableView.dataSource = dataSource
self.dataSource = dataSource
title = L10n.Scene.Settings.Notifications.Policy.title
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var snapshot = NSDiffableDataSourceSnapshot<NotificationPolicySection, NotificationPolicy>()
snapshot.appendSections(sections)
snapshot.appendItems(NotificationPolicy.allCases)
dataSource?.apply(snapshot)
}
}
extension PolicySelectionViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let newPolicy = sections[indexPath.section].entries[indexPath.row]
viewModel.selectedPolicy = newPolicy
viewModel.updated = true
if let dataSource {
dataSource.applySnapshotUsingReloadData(dataSource.snapshot())
}
delegate?.newPolicySelected(self, newPolicy: newPolicy)
tableView.deselectRow(at: indexPath, animated: true)
}
}

View File

@ -0,0 +1,72 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
struct SettingsSection: Hashable {
let entries: [SettingsEntry]
}
enum SettingsEntry: Hashable {
case general
case notifications
case aboutMastodon
case logout(accountName: String)
var title: String {
switch self {
case .general:
return L10n.Scene.Settings.Overview.general
case .notifications:
return L10n.Scene.Settings.Overview.notifications
case .aboutMastodon:
return L10n.Scene.Settings.Overview.aboutMastodon
case .logout(let accountName):
return L10n.Scene.Settings.Overview.logout(accountName)
}
}
var accessoryType: UITableViewCell.AccessoryType {
switch self {
case .general, .notifications, .aboutMastodon, .logout(_):
return .disclosureIndicator
}
}
var icon: UIImage? {
switch self {
case .general:
return UIImage(systemName: "gear")
case .notifications:
return UIImage(systemName: "bell.badge")
case .aboutMastodon:
return UIImage(systemName: "info.circle.fill")
case .logout(_):
return nil
}
}
var iconBackgroundColor: UIColor? {
switch self {
case .general:
return .systemGray
case .notifications:
return .systemRed
case .aboutMastodon:
return .systemPurple
case .logout(_):
return nil
}
}
var textColor: UIColor {
switch self {
case .general, .notifications, .aboutMastodon:
return .label
case .logout(_):
return .red
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
class SettingsTableViewCell: UITableViewCell {
static let reuseIdentifier = "SettingsTableViewCell"
let iconImageView: UIImageView
let iconImageBackgroundView: UIView
let titleLabel: UILabel
private let contentStackView: UIStackView
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
iconImageView = UIImageView()
iconImageView.translatesAutoresizingMaskIntoConstraints = false
iconImageBackgroundView = UIView()
iconImageBackgroundView.addSubview(iconImageView)
titleLabel = UILabel()
contentStackView = UIStackView(arrangedSubviews: [iconImageBackgroundView, titleLabel])
contentStackView.translatesAutoresizingMaskIntoConstraints = false
contentStackView.axis = .horizontal
contentStackView.alignment = .center
contentStackView.spacing = 16
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(contentStackView)
setupConstraints()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupConstraints() {
let constraints = [
contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
contentView.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor, constant: 8),
contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 8),
iconImageBackgroundView.heightAnchor.constraint(equalToConstant: 30),
iconImageBackgroundView.widthAnchor.constraint(equalTo: iconImageBackgroundView.heightAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconImageBackgroundView.centerYAnchor),
iconImageView.centerXAnchor.constraint(equalTo: iconImageBackgroundView.centerXAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 20),
titleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 12),
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor, constant: 12),
]
NSLayoutConstraint.activate(constraints)
}
func update(with entry: SettingsEntry) {
titleLabel.textColor = entry.textColor
titleLabel.text = entry.title
if let icon = entry.icon {
iconImageView.image = icon
iconImageView.tintColor = .white
iconImageBackgroundView.isHidden = false
} else {
iconImageBackgroundView.isHidden = true
}
iconImageBackgroundView.layer.cornerRadius = 5
iconImageBackgroundView.backgroundColor = entry.iconBackgroundColor
accessoryType = entry.accessoryType
}
}

View File

@ -0,0 +1,91 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
protocol SettingsViewControllerDelegate: AnyObject {
func done(_ viewController: UIViewController)
func didSelect(_ viewController: UIViewController, entry: SettingsEntry)
}
class SettingsViewController: UIViewController {
let sections: [SettingsSection]
weak var delegate: SettingsViewControllerDelegate?
var tableViewDataSource: UITableViewDiffableDataSource<SettingsSection, SettingsEntry>?
let tableView: UITableView
init(accountName: String) {
sections = [
.init(entries: [.general, .notifications]),
.init(entries: [.aboutMastodon]),
.init(entries: [.logout(accountName: accountName)])
]
tableView = UITableView(frame: .zero, style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.reuseIdentifier)
super.init(nibName: nil, bundle: nil)
let tableViewDataSource = UITableViewDiffableDataSource<SettingsSection, SettingsEntry>(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in
guard let self,
let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell
else { fatalError("Wrong cell WTF??") }
let entry = self.sections[indexPath.section].entries[indexPath.row]
cell.update(with: entry)
return cell
}
tableView.dataSource = tableViewDataSource
tableView.delegate = self
self.tableViewDataSource = tableViewDataSource
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(SettingsViewController.done(_:)))
view.backgroundColor = .systemGroupedBackground
view.addSubview(tableView)
title = L10n.Scene.Settings.Overview.title
tableView.pinToParent()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsEntry>()
for section in sections {
snapshot.appendSections([section])
snapshot.appendItems(section.entries)
}
tableViewDataSource?.apply(snapshot)
}
//MARK: Actions
@objc
func done(_ sender: Any) {
delegate?.done(self)
}
}
//MARK: UITableViewDelegate
extension SettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = sections[indexPath.section].entries[indexPath.row]
delegate?.didSelect(self, entry: entry)
tableView.deselectRow(at: indexPath, animated: true)
}
}

View File

@ -0,0 +1,191 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import AuthenticationServices
import MastodonCore
import CoreDataStack
import MastodonSDK
import Combine
protocol SettingsCoordinatorDelegate: AnyObject {
func logout(_ settingsCoordinator: SettingsCoordinator)
func openGithubURL(_ settingsCoordinator: SettingsCoordinator)
func openPrivacyURL(_ settingsCoordinator: SettingsCoordinator)
func openProfileSettingsURL(_ settingsCoordinator: SettingsCoordinator)
}
class SettingsCoordinator: NSObject, Coordinator {
let navigationController: UINavigationController
let presentedOn: UIViewController
weak var delegate: SettingsCoordinatorDelegate?
private let settingsViewController: SettingsViewController
let setting: Setting
let appContext: AppContext
let authContext: AuthContext
var disposeBag = Set<AnyCancellable>()
init(presentedOn: UIViewController, accountName: String, setting: Setting, appContext: AppContext, authContext: AuthContext) {
self.presentedOn = presentedOn
navigationController = UINavigationController()
self.setting = setting
self.appContext = appContext
self.authContext = authContext
settingsViewController = SettingsViewController(accountName: accountName)
}
func start() {
settingsViewController.delegate = self
navigationController.pushViewController(settingsViewController, animated: false)
presentedOn.present(navigationController, animated: true)
}
}
//MARK: - SettingsViewControllerDelegate
extension SettingsCoordinator: SettingsViewControllerDelegate {
func done(_ viewController: UIViewController) {
viewController.dismiss(animated: true)
}
func didSelect(_ viewController: UIViewController, entry: SettingsEntry) {
switch entry {
case .general:
let generalSettingsViewController = GeneralSettingsViewController(setting: setting)
generalSettingsViewController.delegate = self
navigationController.pushViewController(generalSettingsViewController, animated: true)
case .notifications:
let currentSetting = appContext.settingService.currentSetting.value
let notificationsEnabled = appContext.notificationService.isNotificationPermissionGranted.value
let notificationViewController = NotificationSettingsViewController(currentSetting: currentSetting, notificationsEnabled: notificationsEnabled)
notificationViewController.delegate = self
self.navigationController.pushViewController(notificationViewController, animated: true)
case .aboutMastodon:
let aboutViewController = AboutViewController()
aboutViewController.delegate = self
navigationController.pushViewController(aboutViewController, animated: true)
case .logout(_):
delegate?.logout(self)
}
}
}
//MARK: - AboutViewControllerDelegate
extension SettingsCoordinator: AboutViewControllerDelegate {
func didSelect(_ viewController: AboutViewController, entry: AboutSettingsEntry) {
switch entry {
case .evenMoreSettings:
delegate?.openProfileSettingsURL(self)
case .contributeToMastodon:
delegate?.openGithubURL(self)
case .privacyPolicy:
delegate?.openPrivacyURL(self)
case .clearMediaCache(_):
//FIXME: maybe we should inject an AppContext/AuthContext here instead of delegating everything to SceneCoordinator?
AppContext.shared.purgeCache()
viewController.update(with:
[AboutSettingsSection(entries: [
.evenMoreSettings,
.contributeToMastodon,
.privacyPolicy
]),
AboutSettingsSection(entries: [
.clearMediaCache(AppContext.shared.currentDiskUsage())
])]
)
}
}
}
//MARK: - ASWebAuthenticationPresentationContextProviding
extension SettingsCoordinator: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return navigationController.view.window!
}
}
//MARK: - GeneralSettingsViewControllerDelegate
extension SettingsCoordinator: GeneralSettingsViewControllerDelegate {
func save(_ viewController: UIViewController, setting: Setting, viewModel: GeneralSettingsViewModel) {
UserDefaults.shared.customUserInterfaceStyle = viewModel.selectedAppearence.interfaceStyle
UserDefaults.shared.preferredStaticEmoji = viewModel.playAnimations == false
UserDefaults.shared.preferredStaticAvatar = viewModel.playAnimations == false
UserDefaults.shared.preferredUsingDefaultBrowser = viewModel.selectedOpenLinks == .browser
}
}
//MARK: - NotificationSettingsViewControllerDelegate
extension SettingsCoordinator: NotificationSettingsViewControllerDelegate {
func showPolicyList(_ viewController: UIViewController, viewModel: NotificationSettingsViewModel) {
let policyListViewController = PolicySelectionViewController(viewModel: viewModel)
policyListViewController.delegate = self
navigationController.pushViewController(policyListViewController, animated: true)
}
func viewWillDisappear(_ viewController: UIViewController, viewModel: NotificationSettingsViewModel) {
guard viewModel.updated else { return }
let authenticationBox = authContext.mastodonAuthenticationBox
guard let subscription = setting.activeSubscription,
setting.domain == authenticationBox.domain,
setting.userID == authenticationBox.userID,
let legacyViewModel = appContext.notificationService.dequeueNotificationViewModel(mastodonAuthenticationBox: authenticationBox), let deviceToken = appContext.notificationService.deviceToken.value else { return }
let queryData = Mastodon.API.Subscriptions.QueryData(
policy: viewModel.selectedPolicy.subscriptionPolicy,
alerts: Mastodon.API.Subscriptions.QueryData.Alerts(
favourite: viewModel.notifyFavorites,
follow: viewModel.notifyNewFollowers,
reblog: viewModel.notifyBoosts,
mention: viewModel.notifyMentions,
poll: subscription.alert.poll
)
)
let query = legacyViewModel.createSubscribeQuery(
deviceToken: deviceToken,
queryData: queryData,
mastodonAuthenticationBox: authenticationBox
)
appContext.apiService.createSubscription(
subscriptionObjectID: subscription.objectID,
query: query,
mastodonAuthenticationBox: authenticationBox
).sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { output in
print(output)
})
.store(in: &disposeBag)
}
func showNotificationSettings(_ viewController: UIViewController) {
if #available(iOS 16.0, *) {
if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
UIApplication.shared.open(url)
}
} else {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
}
//MARK: - PolicySelectionViewControllerDelegate
extension SettingsCoordinator: PolicySelectionViewControllerDelegate {
func newPolicySelected(_ viewController: PolicySelectionViewController, newPolicy: NotificationPolicy) {
self.setting.activeSubscription?.policyRaw = newPolicy.subscriptionPolicy.rawValue
try? self.appContext.managedObjectContext.save()
}
}

View File

@ -1,569 +0,0 @@
//
// SettingsViewController.swift
// Mastodon
//
// Created by ihugo on 2021/4/7.
//
import UIKit
import Combine
import CoreData
import CoreDataStack
import AuthenticationServices
import MetaTextKit
import MastodonSDK
import MastodonMeta
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
class SettingsViewController: UIViewController, NeedsDependency {
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var viewModel: SettingsViewModel! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var notificationPolicySubscription: AnyCancellable?
var triggerMenu: UIMenu {
let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone
let follower = L10n.Scene.Settings.Section.Notifications.Trigger.follower
let follow = L10n.Scene.Settings.Section.Notifications.Trigger.follow
let noOne = L10n.Scene.Settings.Section.Notifications.Trigger.noone
let menu = UIMenu(
image: nil,
identifier: nil,
options: .displayInline,
children: [
UIAction(title: anyone, image: UIImage(systemName: "person.3"), attributes: []) { [weak self] action in
self?.updateTrigger(policy: .all)
},
UIAction(title: follower, image: UIImage(systemName: "person.crop.circle.badge.plus"), attributes: []) { [weak self] action in
self?.updateTrigger(policy: .follower)
},
UIAction(title: follow, image: UIImage(systemName: "person.crop.circle.badge.checkmark"), attributes: []) { [weak self] action in
self?.updateTrigger(policy: .followed)
},
UIAction(title: noOne, image: UIImage(systemName: "nosign"), attributes: []) { [weak self] action in
self?.updateTrigger(policy: .none)
},
]
)
return menu
}
private let notifySectionHeaderStackView: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isLayoutMarginsRelativeArrangement = true
view.axis = .horizontal
view.spacing = 4
return view
}()
let notifyLabel = UILabel()
private(set) lazy var notifySectionHeader: UIView = {
let view = notifySectionHeaderStackView
notifyLabel.translatesAutoresizingMaskIntoConstraints = false
notifyLabel.adjustsFontForContentSizeCategory = true
notifyLabel.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
notifyLabel.textColor = Asset.Colors.Label.primary.color
notifyLabel.text = L10n.Scene.Settings.Section.Notifications.Trigger.title
notifyLabel.adjustsFontSizeToFitWidth = true
notifyLabel.minimumScaleFactor = 0.5
view.addArrangedSubview(notifyLabel)
view.addArrangedSubview(whoButton)
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .horizontal)
whoButton.setContentHuggingPriority(.defaultHigh + 1, for: .vertical)
return view
}()
private(set) lazy var whoButton: UIButton = {
let whoButton = UIButton(type: .roundedRect)
whoButton.menu = triggerMenu
whoButton.showsMenuAsPrimaryAction = true
whoButton.setBackgroundColor(Asset.Colors.battleshipGrey.color, for: .normal)
whoButton.setTitleColor(Asset.Colors.Label.primary.color, for: .normal)
whoButton.titleLabel?.adjustsFontForContentSizeCategory = true
whoButton.titleLabel?.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold))
whoButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
whoButton.layer.cornerRadius = 10
whoButton.clipsToBounds = true
whoButton.titleLabel?.adjustsFontSizeToFitWidth = true
whoButton.titleLabel?.minimumScaleFactor = 0.5
return whoButton
}()
private(set) lazy var tableView: UITableView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Width' UIStackView:0x7f8c2b6c0590.width == 0)
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), style: .insetGrouped)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.rowHeight = UITableView.automaticDimension
tableView.backgroundColor = .clear
tableView.separatorColor = ThemeService.shared.currentTheme.value.separator
tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self))
tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self))
tableView.register(SettingsLinkTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsLinkTableViewCell.self))
return tableView
}()
let tableFooterLabel = MetaLabel(style: .settingTableFooter)
lazy var tableFooterView: UIView = {
// init with a frame to fix a conflict ('UIView-Encapsulated-Layout-Height' UIStackView:0x7ffe41e47da0.height == 0)
let view = UIStackView(frame: CGRect(x: 0, y: 0, width: 320, height: 320))
view.isLayoutMarginsRelativeArrangement = true
view.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
view.axis = .vertical
view.alignment = .center
// tableFooterLabel.linkDelegate = self
view.addArrangedSubview(tableFooterLabel)
return view
}()
}
extension SettingsViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupView()
bindViewModel()
viewModel.viewDidLoad.send()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// make large title not collapsed
navigationController?.navigationBar.sizeToFit()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let footerView = self.tableView.tableFooterView else {
return
}
let width = self.tableView.bounds.size.width
let size = footerView.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height))
if footerView.frame.size.height != size.height {
footerView.frame.size.height = size.height
self.tableView.tableFooterView = footerView
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateSectionHeaderStackViewLayout()
}
// MAKR: - Private methods
private func updateSectionHeaderStackViewLayout() {
// accessibility
if traitCollection.preferredContentSizeCategory < .accessibilityMedium {
notifySectionHeaderStackView.axis = .horizontal
notifyLabel.numberOfLines = 1
} else {
notifySectionHeaderStackView.axis = .vertical
notifyLabel.numberOfLines = 0
}
}
private func bindViewModel() {
self.whoButton.setTitle(viewModel.setting.value.activeSubscription?.policy.title, for: .normal)
viewModel.setting
.sink { [weak self] setting in
guard let self = self else { return }
self.notificationPolicySubscription = ManagedObjectObserver.observe(object: setting)
.sink { _ in
// do nothing
} receiveValue: { [weak self] change in
guard let self = self else { return }
guard case let .update(object) = change.changeType,
let setting = object as? Setting else { return }
if let activeSubscription = setting.activeSubscription {
self.whoButton.setTitle(activeSubscription.policy.title, for: .normal)
} else {
// assertionFailure()
}
}
}
.store(in: &disposeBag)
let footer = "Mastodon for iOS v\(UIApplication.appVersion()) (\(UIApplication.appBuild()))"
let metaContent = PlaintextMetaContent(string: footer)
tableFooterLabel.configure(content: metaContent)
}
private func setupView() {
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &disposeBag)
setupNavigation()
view.addSubview(tableView)
tableView.pinToParent()
setupTableView()
updateSectionHeaderStackViewLayout()
}
private func setupBackgroundColor(theme: Theme) {
view.backgroundColor = UIColor(dynamicProvider: { traitCollection in
switch traitCollection.userInterfaceLevel {
case .elevated where traitCollection.userInterfaceStyle == .dark:
return theme.systemElevatedBackgroundColor
default:
return theme.secondarySystemBackgroundColor
}
})
tableView.separatorColor = theme.separator
}
private func setupNavigation() {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.rightBarButtonItem
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.done,
target: self,
action: #selector(doneButtonDidClick))
navigationItem.title = L10n.Scene.Settings.title
}
private func setupTableView() {
viewModel.setupDiffableDataSource(
for: tableView,
settingsAppearanceTableViewCellDelegate: self,
settingsToggleCellDelegate: self
)
tableView.tableFooterView = tableFooterView
}
func alertToSignOut() {
let alertController = UIAlertController(
title: L10n.Common.Alerts.SignOut.title,
message: L10n.Common.Alerts.SignOut.message,
preferredStyle: .alert
)
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
let signOutAction = UIAlertAction(title: L10n.Common.Alerts.SignOut.confirm, style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.signOut()
}
alertController.addAction(cancelAction)
alertController.addAction(signOutAction)
_ = self.coordinator.present(
scene: .alertController(alertController: alertController),
from: self,
transition: .alertController(animated: true, completion: nil)
)
}
func signOut() {
// clear badge before sign-out
context.notificationService.clearNotificationCountForActiveUser()
Task { @MainActor in
try await context.authenticationService.signOutMastodonUser(
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
)
self.coordinator.setup()
}
}
}
// Mark: - Actions
extension SettingsViewController {
@objc private func doneButtonDidClick() {
dismiss(animated: true, completion: nil)
}
}
// MARK: - UITableViewDelegate
extension SettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sections = viewModel.dataSource.snapshot().sectionIdentifiers
guard section < sections.count else { return nil }
let sectionIdentifier = sections[section]
let header: SettingsSectionHeader
switch sectionIdentifier {
case .preference:
return UIView()
case .notifications:
header = SettingsSectionHeader(
frame: CGRect(x: 0, y: 0, width: 375, height: 66),
customView: notifySectionHeader)
header.update(title: sectionIdentifier.title)
default:
header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66))
header.update(title: sectionIdentifier.title)
}
header.preservesSuperviewLayoutMargins = true
return header
}
// remove the gap of table's footer
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return UIView()
}
// remove the gap of table's footer
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNonzeroMagnitude
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let dataSource = viewModel.dataSource else { return }
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .appearance:
// do nothing
break
case .notification:
// do nothing
break
case .preference:
// do nothing
break
case .boringZone(let link), .spicyZone(let link):
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
feedbackGenerator.impactOccurred()
switch link {
case .accountSettings:
let domain = viewModel.authContext.mastodonAuthenticationBox.domain
guard let url = URL(string: "https://\(domain)/auth/edit") else { return }
viewModel.openAuthenticationPage(authenticateURL: url, presentationContextProvider: self)
case .github:
guard let url = URL(string: "https://github.com/mastodon/mastodon-ios") else { break }
_ = coordinator.present(
scene: .safari(url: url),
from: self,
transition: .safariPresent(animated: true, completion: nil)
)
case .termsOfService, .privacyPolicy:
// same URL
guard let url = viewModel.privacyURL else { break }
_ = coordinator.present(
scene: .safari(url: url),
from: self,
transition: .safariPresent(animated: true, completion: nil)
)
case .clearMediaCache:
context.purgeCache()
.receive(on: RunLoop.main)
.sink { [weak self] byteCount in
guard let self = self else { return }
let byteCountFormatted = AppContext.byteCountFormatter.string(fromByteCount: Int64(byteCount))
let alertController = UIAlertController(
title: L10n.Common.Alerts.CleanCache.title,
message: L10n.Common.Alerts.CleanCache.message(byteCountFormatted),
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))
}
.store(in: &disposeBag)
case .signOut:
feedbackGenerator.impactOccurred()
alertToSignOut()
}
}
}
}
// Update setting into core data
extension SettingsViewController {
func updateTrigger(policy: Mastodon.API.Subscriptions.Policy) {
let objectID = self.viewModel.setting.value.objectID
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
let setting = managedObjectContext.object(with: objectID) as! Setting
let (subscription, _) = APIService.CoreData.createOrFetchSubscription(
into: managedObjectContext,
setting: setting,
policy: policy
)
let now = Date()
subscription.update(activedAt: now)
setting.didUpdate(at: now)
}
.sink { _ in
// do nothing
} receiveValue: { _ in
// do nothing
}
.store(in: &disposeBag)
}
}
// MARK: - SettingsAppearanceTableViewCellDelegate
extension SettingsViewController: SettingsAppearanceTableViewCellDelegate {
func settingsAppearanceTableViewCell(
_ cell: SettingsAppearanceTableViewCell,
didSelectAppearanceMode appearanceMode: SettingsItem.AppearanceMode
) {
guard let dataSource = viewModel.dataSource else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
let item = dataSource.itemIdentifier(for: indexPath)
guard case .appearance = item else { return }
Task { @MainActor in
switch appearanceMode {
case .system:
UserDefaults.shared.customUserInterfaceStyle = .unspecified
case .dark:
UserDefaults.shared.customUserInterfaceStyle = .dark
case .light:
UserDefaults.shared.customUserInterfaceStyle = .light
}
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
feedbackGenerator.impactOccurred()
} // end Task
}
}
extension SettingsViewController: SettingsToggleCellDelegate {
func settingsToggleCell(_ cell: SettingsToggleTableViewCell, switchValueDidChange switch: UISwitch) {
guard let dataSource = viewModel.dataSource else { return }
guard let indexPath = tableView.indexPath(for: cell) else { return }
let isOn = `switch`.isOn
let item = dataSource.itemIdentifier(for: indexPath)
switch item {
case .notification(let record, let switchMode):
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
guard let setting = record.object(in: managedObjectContext) else { return }
guard let subscription = setting.activeSubscription else { return }
let alert = subscription.alert
switch switchMode {
case .favorite: alert.update(favourite: isOn)
case .follow: alert.update(follow: isOn)
case .reblog: alert.update(reblog: isOn)
case .mention: alert.update(mention: isOn)
}
// trigger setting update
alert.subscription.setting?.didUpdate(at: Date())
}
.sink { _ in
// do nothing
}
.store(in: &disposeBag)
case .preference(let record, let preferenceType):
let managedObjectContext = context.backgroundManagedObjectContext
managedObjectContext.performChanges {
guard let setting = record.object(in: managedObjectContext) else { return }
switch preferenceType {
case .disableAvatarAnimation:
setting.update(preferredStaticAvatar: isOn)
case .disableEmojiAnimation:
setting.update(preferredStaticEmoji: isOn)
case .useDefaultBrowser:
setting.update(preferredUsingDefaultBrowser: isOn)
}
}
.sink { result in
switch result {
case .success:
switch preferenceType {
case .disableAvatarAnimation:
UserDefaults.shared.preferredStaticAvatar = isOn
case .disableEmojiAnimation:
UserDefaults.shared.preferredStaticEmoji = isOn
case .useDefaultBrowser:
UserDefaults.shared.preferredUsingDefaultBrowser = isOn
}
case .failure(let error):
assertionFailure(error.localizedDescription)
break
}
}
.store(in: &disposeBag)
default:
assertionFailure()
break
}
}
}
// MARK: - MetaLabelDelegate
extension SettingsViewController: MetaLabelDelegate {
func metaLabel(_ metaLabel: MetaLabel, didSelectMeta meta: Meta) {
switch meta {
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
_ = coordinator.present(scene: .safari(url: url), from: self, transition: .safariPresent(animated: true, completion: nil))
default:
assertionFailure()
}
}
}
// MARK: - ASAuthorizationControllerPresentationContextProviding
extension SettingsViewController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return view.window!
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
extension SettingsViewController: UIAdaptivePresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .pageSheet
}
}
extension SettingsViewController {
var closeKeyCommand: UIKeyCommand {
UIKeyCommand(
title: L10n.Scene.Settings.Keyboard.closeSettingsWindow,
image: nil,
action: #selector(SettingsViewController.closeSettingsWindowKeyCommandHandler(_:)),
input: "w",
modifierFlags: .command,
propertyList: nil,
alternates: [],
discoverabilityTitle: nil,
attributes: [],
state: .off
)
}
override var keyCommands: [UIKeyCommand]? {
return [closeKeyCommand]
}
@objc private func closeSettingsWindowKeyCommandHandler(_ sender: UIKeyCommand) {
dismiss(animated: true, completion: nil)
}
}

View File

@ -1,164 +0,0 @@
//
// SettingsViewModel.swift
// Mastodon
//
// Created by ihugo on 2021/4/7.
//
import Combine
import CoreData
import CoreDataStack
import Foundation
import MastodonSDK
import UIKit
import AuthenticationServices
import MastodonCore
class SettingsViewModel {
var disposeBag = Set<AnyCancellable>()
// input
let context: AppContext
let authContext: AuthContext
var mastodonAuthenticationController: MastodonAuthenticationController?
let setting: CurrentValueSubject<Setting, Never>
var updateDisposeBag = Set<AnyCancellable>()
var createDisposeBag = Set<AnyCancellable>()
let viewDidLoad = PassthroughSubject<Void, Never>()
// output
var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
/// create a subscription when:
/// - does not has one
/// - does not find subscription for selected trigger when change trigger
let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
let currentInstance = CurrentValueSubject<Mastodon.Entity.Instance?, Never>(nil)
/// update a subscription when:
/// - change switch for specified alerts
let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
lazy var privacyURL: URL? = {
let domain = authContext.mastodonAuthenticationBox.domain
return Mastodon.API.privacyURL(domain: domain)
}()
init(context: AppContext, authContext: AuthContext, setting: Setting) {
self.context = context
self.authContext = authContext
self.setting = CurrentValueSubject(setting)
self.setting
.sink(receiveValue: { [weak self] setting in
guard let self = self else { return }
self.processDataSource(setting)
})
.store(in: &disposeBag)
context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain)
.sink { [weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(_):
self.currentInstance.value = nil
case .finished:
break
}
} receiveValue: { [weak self] response in
guard let self = self else { return }
self.currentInstance.value = response.value
}
.store(in: &disposeBag)
}
}
extension SettingsViewModel {
func openAuthenticationPage(
authenticateURL: URL,
presentationContextProvider: ASWebAuthenticationPresentationContextProviding
) {
let authenticationController = MastodonAuthenticationController(
context: self.context,
authenticateURL: authenticateURL
)
self.mastodonAuthenticationController = authenticationController
authenticationController.authenticationSession?.presentationContextProvider = presentationContextProvider
authenticationController.authenticationSession?.start()
}
// MARK: - Private methods
private func processDataSource(_ setting: Setting) {
guard let dataSource = self.dataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
// appearance
let appearanceItems = [
SettingsItem.appearance(record: .init(objectID: setting.objectID))
]
snapshot.appendSections([.appearance])
snapshot.appendItems(appearanceItems, toSection: .appearance)
// preference
snapshot.appendSections([.preference])
let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in
SettingsItem.preference(settingRecord: .init(objectID: setting.objectID), preferenceType: preferenceType)
}
snapshot.appendItems(preferenceItems,toSection: .preference)
// notification
let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in
SettingsItem.notification(settingRecord: .init(objectID: setting.objectID), switchMode: mode)
}
snapshot.appendSections([.notifications])
snapshot.appendItems(notificationItems, toSection: .notifications)
// boring zone
let boringZoneSettingsItems: [SettingsItem] = {
let links: [SettingsItem.Link] = [
.accountSettings,
.github,
.termsOfService,
.privacyPolicy
]
let items = links.map { SettingsItem.boringZone(item: $0) }
return items
}()
snapshot.appendSections([.boringZone])
snapshot.appendItems(boringZoneSettingsItems, toSection: .boringZone)
let spicyZoneSettingsItems: [SettingsItem] = {
let links: [SettingsItem.Link] = [
.clearMediaCache,
.signOut
]
let items = links.map { SettingsItem.spicyZone(item: $0) }
return items
}()
snapshot.appendSections([.spicyZone])
snapshot.appendItems(spicyZoneSettingsItems, toSection: .spicyZone)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension SettingsViewModel {
func setupDiffableDataSource(
for tableView: UITableView,
settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
settingsToggleCellDelegate: SettingsToggleCellDelegate
) {
dataSource = SettingsSection.tableViewDiffableDataSource(
for: tableView,
managedObjectContext: context.managedObjectContext,
settingsAppearanceTableViewCellDelegate: settingsAppearanceTableViewCellDelegate,
settingsToggleCellDelegate: settingsToggleCellDelegate
)
processDataSource(self.setting.value)
}
}

View File

@ -0,0 +1,47 @@
// Copyright © 2023 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonAsset
class ToggleTableViewCell: UITableViewCell {
class var reuseIdentifier: String {
return "ToggleTableViewCell"
}
let label: UILabel
let toggle: UISwitch
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
label.numberOfLines = 0
toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
toggle.onTintColor = Asset.Colors.Brand.blurple.color
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(label)
contentView.addSubview(toggle)
setupConstraints()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
private func setupConstraints() {
let constraints = [
label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
contentView.bottomAnchor.constraint(equalTo: label.bottomAnchor, constant: 11),
toggle.leadingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: 16),
toggle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.trailingAnchor.constraint(equalTo: toggle.trailingAnchor, constant: 16)
]
NSLayoutConstraint.activate(constraints)
}
}

View File

@ -1,147 +0,0 @@
//
// AppearanceView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-7-6.
//
import UIKit
import MastodonAsset
import MastodonLocalization
import MastodonUI
class AppearanceView: UIView {
let imageViewShadowBackgroundContainer = ShadowBackgroundContainer()
lazy var imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.layer.masksToBounds = true
view.layer.cornerRadius = 4
view.layer.cornerCurve = .continuous
// accessibility
view.accessibilityIgnoresInvertColors = true
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .regular)
label.textColor = Asset.Colors.Label.primary.color
label.textAlignment = .center
return label
}()
lazy var checkmarkButton: UIButton = {
let button = UIButton()
button.isUserInteractionEnabled = false
button.setImage(UIImage(systemName: "circle"), for: .normal)
button.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected)
button.imageView?.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
button.imageView?.tintColor = Asset.Colors.Label.primary.color
button.imageView?.contentMode = .scaleAspectFill
return button
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 8
view.distribution = .equalSpacing
return view
}()
var selected: Bool = false {
didSet { setNeedsLayout() }
}
// MARK: - Methods
init(image: UIImage?, title: String) {
super.init(frame: .zero)
setupUI()
imageView.image = image
titleLabel.text = title
}
override var isAccessibilityElement: Bool {
get { return true }
set { }
}
override var accessibilityLabel: String? {
get { titleLabel.text }
set { }
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension AppearanceView {
private func setupUI() {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewShadowBackgroundContainer.addSubview(imageView)
imageView.pinToParent()
imageViewShadowBackgroundContainer.cornerRadius = 4
stackView.addArrangedSubview(imageViewShadowBackgroundContainer)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(checkmarkButton)
addSubview(stackView)
translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.pinToParent()
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 121.0 / 100.0), // height / width
])
}
private func configureForSelection() {
if selected {
accessibilityTraits.insert(.selected)
} else {
accessibilityTraits.remove(.selected)
}
checkmarkButton.isSelected = selected
}
override func layoutSubviews() {
super.layoutSubviews()
configureForSelection()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setNeedsLayout()
}
}
extension AppearanceView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
self.alpha = 0.5
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
UIView.animate(withDuration: 0.33) {
self.alpha = 1
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
UIView.animate(withDuration: 0.33) {
self.alpha = 1
}
}
}

View File

@ -1,67 +0,0 @@
//
// SettingsSectionHeader.swift
// Mastodon
//
// Created by ihugo on 2021/4/8.
//
import UIKit
import MastodonAsset
import MastodonLocalization
struct GroupedTableViewConstraints {
static let topMargin: CGFloat = 40
static let bottomMargin: CGFloat = 10
}
/// section header which supports add a custom view blelow the title
class SettingsSectionHeader: UIView {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 13, weight: .regular)
label.textColor = Asset.Colors.Label.secondary.color
return label
}()
lazy var stackView: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isLayoutMarginsRelativeArrangement = true
view.layoutMargins = UIEdgeInsets(
top: GroupedTableViewConstraints.topMargin,
left: 0,
bottom: GroupedTableViewConstraints.bottomMargin,
right: 0
)
view.axis = .vertical
return view
}()
init(frame: CGRect, customView: UIView? = nil) {
super.init(frame: frame)
backgroundColor = .clear
stackView.addArrangedSubview(titleLabel)
if let view = customView {
stackView.addArrangedSubview(view)
}
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: self.readableContentGuide.leadingAnchor),
stackView.trailingAnchor.constraint(lessThanOrEqualTo: self.readableContentGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
stackView.topAnchor.constraint(equalTo: self.topAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(title: String?) {
titleLabel.text = title?.uppercased()
}
}

View File

@ -144,18 +144,7 @@ extension ContentWarningOverlayView {
addGestureRecognizer(tapGestureRecognizer)
configure(style: .media)
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &_disposeBag)
}
private func setupBackgroundColor(theme: Theme) {
contentOverlayView.backgroundColor = theme.contentWarningOverlayBackgroundColor
contentOverlayView.backgroundColor = SystemTheme.contentWarningOverlayBackgroundColor
}
}

View File

@ -21,14 +21,6 @@ extension PollOptionView {
viewModel.objects.insert(option)
// background
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.viewModel.roundedBackgroundViewColor = theme.systemElevatedBackgroundColor
}
.store(in: &disposeBag)
// metaContent
option.publisher(for: \.title)
.map { title -> MetaContent? in
@ -103,41 +95,24 @@ extension PollOptionView {
}
.store(in: &disposeBag)
// appearance
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : theme.tableViewCellSelectionBackgroundColor
})
}
.store(in: &disposeBag)
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
})
}
}
extension PollOptionView {
public func configure(historyPollOption option: StatusEdit.Poll.Option) {
// background
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.viewModel.roundedBackgroundViewColor = theme.systemElevatedBackgroundColor
}
.store(in: &disposeBag)
viewModel.roundedBackgroundViewColor = SystemTheme.systemElevatedBackgroundColor
// metaContent
viewModel.metaContent = PlaintextMetaContent(string: option.title)
// show left-hand-side dots, otherwise view looks "incomplete"
viewModel.selectState = .off
// appearance
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : theme.tableViewCellSelectionBackgroundColor
})
}
.store(in: &disposeBag)
checkmarkBackgroundView.backgroundColor = UIColor(dynamicProvider: { trailtCollection in
return trailtCollection.userInterfaceStyle == .light ? .white : SystemTheme.tableViewCellSelectionBackgroundColor
})
}
}

View File

@ -25,8 +25,8 @@ final class ThreadMetaView: UIView {
let button = UIButton()
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
button.setTitle("0 reblog", for: .normal)
button.setTitleColor(ThemeService.tintColor, for: .normal)
button.setTitleColor(ThemeService.tintColor.withAlphaComponent(0.5), for: .highlighted)
button.setTitleColor(SystemTheme.tintColor, for: .normal)
button.setTitleColor(SystemTheme.tintColor.withAlphaComponent(0.5), for: .highlighted)
return button
}()
@ -34,8 +34,8 @@ final class ThreadMetaView: UIView {
let button = UIButton()
button.titleLabel?.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .semibold))
button.setTitle("0 favorite", for: .normal)
button.setTitleColor(ThemeService.tintColor, for: .normal)
button.setTitleColor(ThemeService.tintColor.withAlphaComponent(0.5), for: .highlighted)
button.setTitleColor(SystemTheme.tintColor, for: .normal)
button.setTitleColor(SystemTheme.tintColor.withAlphaComponent(0.5), for: .highlighted)
return button
}()

View File

@ -26,7 +26,7 @@ final class ThreadReplyLoaderTableViewCell: UITableViewCell {
let loadMoreButton: UIButton = {
let button = HighlightDimmableButton()
button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
button.setTitleColor(ThemeService.tintColor, for: .normal)
button.setTitleColor(SystemTheme.tintColor, for: .normal)
button.setTitle(L10n.Common.Controls.Timeline.Loader.showMoreReplies, for: .normal)
return button
}()
@ -86,14 +86,7 @@ extension ThreadReplyLoaderTableViewCell {
loadMoreButton.addTarget(self, action: #selector(ThreadReplyLoaderTableViewCell.loadMoreButtonDidPressed(_:)), for: .touchUpInside)
setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.setupBackgroundColor(theme: theme)
}
.store(in: &_disposeBag)
backgroundColor = .systemGroupedBackground
}
private func resetSeparatorLineLayout() {
@ -124,11 +117,6 @@ extension ThreadReplyLoaderTableViewCell {
}
}
}
private func setupBackgroundColor(theme: Theme) {
backgroundColor = theme.systemGroupedBackgroundColor
}
}
extension ThreadReplyLoaderTableViewCell {

View File

@ -104,6 +104,8 @@ extension SuggestionAccountViewController: UITableViewDelegate {
return nil
}
footerView.followAllButton.isEnabled = viewModel.userFetchedResultsController.records.isNotEmpty
footerView.delegate = self
return footerView
}

View File

@ -66,7 +66,7 @@ final class SuggestionAccountViewModel: NSObject {
}
guard !userIDs.isEmpty else { return }
guard userIDs.isNotEmpty else { return }
userFetchedResultsController.userIDs = userIDs
}

View File

@ -14,20 +14,20 @@ class SuggestionAccountTableViewFooter: UITableViewHeaderFooterView {
weak var delegate: SuggestionAccountTableViewFooterDelegate?
let followAllButton: FollowButton
let followAllButton: UIButton
override init(reuseIdentifier: String?) {
//TODO: Check if we can use UIButton.configuration here instead?
followAllButton = FollowButton()
followAllButton.translatesAutoresizingMaskIntoConstraints = false
followAllButton.setTitle(L10n.Scene.SuggestionAccount.followAll, for: .normal)
followAllButton.setBackgroundColor(Asset.Colors.Button.userFollow.color, for: .normal)
followAllButton.setTitleColor(.white, for: .normal)
followAllButton.contentEdgeInsets = .init(horizontal: 20, vertical: 12)
followAllButton.cornerRadius = 10
followAllButton.titleLabel?.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15))
var buttonConfiguration = UIButton.Configuration.filled()
buttonConfiguration.baseForegroundColor = .white
buttonConfiguration.baseBackgroundColor = Asset.Colors.Button.userFollow.color
buttonConfiguration.background.cornerRadius = 10
buttonConfiguration.attributedTitle = AttributedString(L10n.Scene.SuggestionAccount.followAll, attributes: AttributeContainer([.font: UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .boldSystemFont(ofSize: 15))]))
buttonConfiguration.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 20, bottom: 12, trailing: 20)
followAllButton = UIButton(configuration: buttonConfiguration)
followAllButton.isEnabled = false
followAllButton.translatesAutoresizingMaskIntoConstraints = false
followAllButton.setContentCompressionResistancePriority(.required, for: .horizontal)
followAllButton.setContentHuggingPriority(.required, for: .horizontal)

View File

@ -49,13 +49,13 @@ class StatusEditHistoryViewController: UIViewController {
}
tableView.dataSource = tableViewDataSource
tableView.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
tableView.backgroundColor = .secondarySystemBackground
self.tableViewDataSource = tableViewDataSource
view.addSubview(tableView)
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
view.backgroundColor = .secondarySystemBackground
setupConstraints()
title = L10n.Common.Controls.Status.EditHistory.title

View File

@ -51,14 +51,7 @@ extension ThreadViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
ThemeService.shared.currentTheme
.receive(on: DispatchQueue.main)
.sink { [weak self] theme in
guard let self = self else { return }
self.view.backgroundColor = theme.secondarySystemBackgroundColor
}
.store(in: &disposeBag)
view.backgroundColor = .secondarySystemBackground
navigationItem.title = L10n.Scene.Thread.backTitle
navigationItem.titleView = titleView

View File

@ -22,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
AppSecret.default.register()
// configure appearance
ThemeService.shared.apply(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.apply()
// configure AudioSession
try? AVAudioSession.sharedInstance().setCategory(.ambient)

View File

@ -37,19 +37,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// set tint color
window.tintColor = UIColor.label
ThemeService.shared.currentTheme
.receive(on: RunLoop.main)
.dropFirst()
.sink { [weak self] theme in
guard let self = self else { return }
guard let window = self.window else { return }
window.subviews.forEach { view in
view.removeFromSuperview()
window.addSubview(view)
}
}
.store(in: &disposeBag)
let appContext = AppContext.shared
let sceneCoordinator = SceneCoordinator(scene: scene, sceneDelegate: self, appContext: appContext)
self.coordinator = sceneCoordinator

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>CoreData 8.xcdatamodel</string>
<string>CoreData 9.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Application" representedClassName="CoreDataStack.Application" syncable="YES">
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="vapidKey" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="application" inverseEntity="Status"/>
</entity>
<entity name="Card" representedClassName="CoreDataStack.Card" syncable="YES">
<attribute name="authorName" optional="YES" attributeType="String"/>
<attribute name="authorURLRaw" optional="YES" attributeType="String"/>
<attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="desc" attributeType="String"/>
<attribute name="embedURLRaw" optional="YES" attributeType="String"/>
<attribute name="height" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="html" optional="YES" attributeType="String"/>
<attribute name="image" optional="YES" attributeType="String"/>
<attribute name="providerName" optional="YES" attributeType="String"/>
<attribute name="providerURLRaw" optional="YES" attributeType="String"/>
<attribute name="title" attributeType="String"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="urlRaw" attributeType="String"/>
<attribute name="width" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="card" inverseEntity="Status"/>
</entity>
<entity name="DomainBlock" representedClassName="CoreDataStack.DomainBlock" syncable="YES">
<attribute name="blockedDomain" attributeType="String"/>
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="userID"/>
<constraint value="domain"/>
<constraint value="blockedDomain"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Emoji" representedClassName="CoreDataStack.Emoji" syncable="YES">
<attribute name="category" optional="YES" attributeType="String"/>
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="shortcode" attributeType="String"/>
<attribute name="staticURL" attributeType="String"/>
<attribute name="url" attributeType="String"/>
<attribute name="visibleInPicker" attributeType="Boolean" usesScalarValueType="YES"/>
</entity>
<entity name="Feed" representedClassName="CoreDataStack.Feed" syncable="YES">
<attribute name="acctRaw" optional="YES" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hasMore" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isLoadingMore" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="kindRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="notification" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Notification" inverseName="feeds" inverseEntity="Notification"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="feeds" inverseEntity="Status"/>
</entity>
<entity name="Instance" representedClassName="CoreDataStack.Instance" syncable="YES">
<attribute name="configurationRaw" optional="YES" attributeType="Binary"/>
<attribute name="configurationV2Raw" optional="YES" attributeType="Binary"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="version" optional="YES" attributeType="String"/>
<relationship name="authentications" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="instance" inverseEntity="MastodonAuthentication"/>
</entity>
<entity name="MastodonAuthentication" representedClassName="CoreDataStack.MastodonAuthentication" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="appAccessToken" attributeType="String"/>
<attribute name="clientID" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userAccessToken" attributeType="String"/>
<attribute name="userID" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="instance" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Instance" inverseName="authentications" inverseEntity="Instance"/>
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
</entity>
<entity name="MastodonUser" representedClassName="CoreDataStack.MastodonUser" syncable="YES">
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" attributeType="String"/>
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="displayName" attributeType="String"/>
<attribute name="domain" attributeType="String"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="fields" optional="YES" attributeType="Binary"/>
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" attributeType="String"/>
<attribute name="headerStatic" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="suspended" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="username" attributeType="String"/>
<relationship name="blocking" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blockingBy" inverseEntity="MastodonUser"/>
<relationship name="blockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="blocking" inverseEntity="MastodonUser"/>
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="bookmarkedBy" inverseEntity="Status"/>
<relationship name="domainBlocking" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlockingBy" inverseEntity="MastodonUser"/>
<relationship name="domainBlockingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="domainBlocking" inverseEntity="MastodonUser"/>
<relationship name="endorsed" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsedBy" inverseEntity="MastodonUser"/>
<relationship name="endorsedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="endorsed" inverseEntity="MastodonUser"/>
<relationship name="favourite" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="favouritedBy" inverseEntity="Status"/>
<relationship name="followedTags" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Tag" inverseName="followedBy" inverseEntity="Tag"/>
<relationship name="following" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followingBy" inverseEntity="MastodonUser"/>
<relationship name="followingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="following" inverseEntity="MastodonUser"/>
<relationship name="followRequested" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequestedBy" inverseEntity="MastodonUser"/>
<relationship name="followRequestedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followRequested" inverseEntity="MastodonUser"/>
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
<relationship name="muted" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="mutedBy" inverseEntity="Status"/>
<relationship name="muting" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mutingBy" inverseEntity="MastodonUser"/>
<relationship name="mutingBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muting" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Nullify" destinationEntity="Notification" inverseName="account" inverseEntity="Notification"/>
<relationship name="pinnedStatus" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="pinnedBy" inverseEntity="Status"/>
<relationship name="privateNotes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="to" inverseEntity="PrivateNote"/>
<relationship name="privateNotesTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PrivateNote" inverseName="from" inverseEntity="PrivateNote"/>
<relationship name="reblogged" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="rebloggedBy" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="account" inverseEntity="SearchHistory"/>
<relationship name="showingReblogs" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogsBy" inverseEntity="MastodonUser"/>
<relationship name="showingReblogsBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="showingReblogs" inverseEntity="MastodonUser"/>
<relationship name="statuses" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="author" inverseEntity="Status"/>
<relationship name="votePollOptions" toMany="YES" deletionRule="Nullify" destinationEntity="PollOption" inverseName="votedBy" inverseEntity="PollOption"/>
<relationship name="votePolls" toMany="YES" deletionRule="Nullify" destinationEntity="Poll" inverseName="votedBy" inverseEntity="Poll"/>
</entity>
<entity name="Notification" representedClassName="CoreDataStack.Notification" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="followRequestState" optional="YES" attributeType="Binary"/>
<attribute name="id" attributeType="String"/>
<attribute name="transientFollowRequestState" optional="YES" transient="YES" attributeType="Binary"/>
<attribute name="typeRaw" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="account" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="notifications" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="notification" inverseEntity="Feed"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="notifications" inverseEntity="Status"/>
</entity>
<entity name="Poll" representedClassName="CoreDataStack.Poll" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="expired" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="expiresAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="isVoting" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="multiple" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votersCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="votesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="options" toMany="YES" deletionRule="Cascade" destinationEntity="PollOption" inverseName="poll" inverseEntity="PollOption"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="poll" inverseEntity="Status"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePolls" inverseEntity="MastodonUser"/>
</entity>
<entity name="PollOption" representedClassName="CoreDataStack.PollOption" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isSelected" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="title" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="votesCount" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Poll" inverseName="options" inverseEntity="Poll"/>
<relationship name="votedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="votePollOptions" inverseEntity="MastodonUser"/>
</entity>
<entity name="PrivateNote" representedClassName="CoreDataStack.PrivateNote" syncable="YES">
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="from" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotesTo" inverseEntity="MastodonUser"/>
<relationship name="to" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="privateNotes" inverseEntity="MastodonUser"/>
</entity>
<entity name="SearchHistory" representedClassName="CoreDataStack.SearchHistory" syncable="YES">
<attribute name="createAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String" defaultValueString=""/>
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="searchHistories" inverseEntity="MastodonUser"/>
<relationship name="hashtag" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Tag" inverseName="searchHistories" inverseEntity="Tag"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="searchHistories" inverseEntity="Status"/>
</entity>
<entity name="Setting" representedClassName="CoreDataStack.Setting" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="rawRecentLanguages" optional="YES" attributeType="Binary"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="String"/>
<relationship name="subscriptions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Subscription" inverseName="setting" inverseEntity="Subscription"/>
</entity>
<entity name="Status" representedClassName="CoreDataStack.Status" syncable="YES">
<attribute name="attachments" optional="YES" attributeType="Binary"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="deletedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String"/>
<attribute name="editedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="favouritesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="inReplyToAccountID" optional="YES" attributeType="String"/>
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
<attribute name="isSensitiveToggled" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="mentions" optional="YES" attributeType="Binary"/>
<attribute name="reblogsCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" optional="YES" attributeType="Integer 64" usesScalarValueType="NO"/>
<attribute name="revealedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sensitive" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="translatedContent" optional="YES" transient="YES" attributeType="Transformable"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="String"/>
<attribute name="visibilityRaw" optional="YES" attributeType="String" elementID="visibility"/>
<relationship name="application" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Application" inverseName="status" inverseEntity="Application"/>
<relationship name="author" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="statuses" inverseEntity="MastodonUser"/>
<relationship name="bookmarkedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="bookmarked" inverseEntity="MastodonUser"/>
<relationship name="card" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Card" inverseName="status" inverseEntity="Card"/>
<relationship name="editHistory" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="StatusEdit" inverseName="status" inverseEntity="StatusEdit"/>
<relationship name="favouritedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="feeds" toMany="YES" deletionRule="Cascade" destinationEntity="Feed" inverseName="status" inverseEntity="Feed"/>
<relationship name="mutedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="notifications" toMany="YES" deletionRule="Cascade" destinationEntity="Notification" inverseName="status" inverseEntity="Notification"/>
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedStatus" inverseEntity="MastodonUser"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="status" inverseEntity="Poll"/>
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="reblogFrom" inverseEntity="Status"/>
<relationship name="reblogFrom" toMany="YES" deletionRule="Cascade" destinationEntity="Status" inverseName="reblog" inverseEntity="Status"/>
<relationship name="rebloggedBy" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="reblogged" inverseEntity="MastodonUser"/>
<relationship name="replyFrom" toMany="YES" deletionRule="Nullify" destinationEntity="Status" inverseName="replyTo" inverseEntity="Status"/>
<relationship name="replyTo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="replyFrom" inverseEntity="Status"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Cascade" destinationEntity="SearchHistory" inverseName="status" inverseEntity="SearchHistory"/>
</entity>
<entity name="StatusEdit" representedClassName="CoreDataStack.StatusEdit" syncable="YES">
<attribute name="attachments" optional="YES" attributeType="Binary"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="emojis" optional="YES" attributeType="Binary"/>
<attribute name="poll" optional="YES" attributeType="Binary"/>
<attribute name="sensitive" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<relationship name="author" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser"/>
<relationship name="status" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status" inverseName="editHistory" inverseEntity="Status"/>
</entity>
<entity name="Subscription" representedClassName="CoreDataStack.Subscription" syncable="YES">
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="endpoint" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="policyRaw" attributeType="String"/>
<attribute name="serverKey" optional="YES" attributeType="String"/>
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="userToken" optional="YES" attributeType="String"/>
<relationship name="alert" maxCount="1" deletionRule="Cascade" destinationEntity="SubscriptionAlerts" inverseName="subscription" inverseEntity="SubscriptionAlerts"/>
<relationship name="setting" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Setting" inverseName="subscriptions" inverseEntity="Setting"/>
</entity>
<entity name="SubscriptionAlerts" representedClassName="CoreDataStack.SubscriptionAlerts" syncable="YES">
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="favouriteRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="followRequestRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mentionRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="pollRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="reblogRaw" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="subscription" maxCount="1" deletionRule="Nullify" destinationEntity="Subscription" inverseName="alert" inverseEntity="Subscription"/>
</entity>
<entity name="Tag" representedClassName="CoreDataStack.Tag" syncable="YES">
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
<attribute name="domain" attributeType="String" defaultValueString=""/>
<attribute name="following" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="histories" optional="YES" attributeType="Binary"/>
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String"/>
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" attributeType="String"/>
<relationship name="followedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="followedTags" inverseEntity="MastodonUser"/>
<relationship name="searchHistories" toMany="YES" deletionRule="Nullify" destinationEntity="SearchHistory" inverseName="hashtag" inverseEntity="SearchHistory"/>
</entity>
</model>

View File

@ -13,12 +13,6 @@ public final class Setting: NSManagedObject {
@NSManaged public var domain: String
@NSManaged public var userID: String
// @NSManaged public var appearanceRaw: String
@NSManaged public var preferredTrueBlackDarkMode: Bool
@NSManaged public var preferredStaticAvatar: Bool
@NSManaged public var preferredStaticEmoji: Bool
@NSManaged public var preferredUsingDefaultBrowser: Bool
@NSManaged public private(set) var createdAt: Date
@NSManaged public private(set) var updatedAt: Date
@ -54,62 +48,27 @@ extension Setting {
property: Property
) -> Setting {
let setting: Setting = context.insertObject()
// setting.appearanceRaw = property.appearanceRaw
setting.domain = property.domain
setting.userID = property.userID
return setting
}
// public func update(appearanceRaw: String) {
// guard appearanceRaw != self.appearanceRaw else { return }
// self.appearanceRaw = appearanceRaw
// didUpdate(at: Date())
// }
public func update(preferredTrueBlackDarkMode: Bool) {
guard preferredTrueBlackDarkMode != self.preferredTrueBlackDarkMode else { return }
self.preferredTrueBlackDarkMode = preferredTrueBlackDarkMode
didUpdate(at: Date())
}
public func update(preferredStaticAvatar: Bool) {
guard preferredStaticAvatar != self.preferredStaticAvatar else { return }
self.preferredStaticAvatar = preferredStaticAvatar
didUpdate(at: Date())
}
public func update(preferredStaticEmoji: Bool) {
guard preferredStaticEmoji != self.preferredStaticEmoji else { return }
self.preferredStaticEmoji = preferredStaticEmoji
didUpdate(at: Date())
}
public func update(preferredUsingDefaultBrowser: Bool) {
guard preferredUsingDefaultBrowser != self.preferredUsingDefaultBrowser else { return }
self.preferredUsingDefaultBrowser = preferredUsingDefaultBrowser
didUpdate(at: Date())
}
public func didUpdate(at networkDate: Date) {
self.updatedAt = networkDate
}
}
extension Setting {
public struct Property {
public let domain: String
public let userID: String
// public let appearanceRaw: String
public init(
domain: String,
userID: String
// appearanceRaw: String
) {
self.domain = domain
self.userID = userID
// self.appearanceRaw = appearanceRaw
}
}
}
@ -127,5 +86,13 @@ extension Setting {
#keyPath(Setting.userID), userID
)
}
}
extension Setting {
public var activeSubscription: Subscription? {
return (subscriptions ?? Set())
.sorted(by: { $0.activedAt > $1.activedAt })
.first
}
}

Some files were not shown because too many files have changed in this diff Show More