mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
Notification Filtering (IOS-241) (#1319)
- Adds a new cell on the "Everything"-notification-screen to show unwanted notifications - Users can define what "unwanted" means to them using the new "Filter"-button in the upper right corner of the Notification-screen - Filtered notifications are sorted by account and users can dismiss/accept if they want to get notifications of that user (it's some standard table-views and delegates) ## Screenshots  
This commit is contained in:
commit
5d8e453da6
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -477,7 +477,15 @@
|
||||
"title": "Home",
|
||||
"timeline_menu": {
|
||||
"following": "Following",
|
||||
"local_community": "Local"
|
||||
"local_community": "Local",
|
||||
"lists": {
|
||||
"title": "Lists",
|
||||
"empty_message": "You don't have any Lists"
|
||||
},
|
||||
"hashtags": {
|
||||
"title": "Followed Hashtags",
|
||||
"empty_message": "You don't follow any Hashtags"
|
||||
}
|
||||
},
|
||||
"timeline_pill": {
|
||||
"offline": "Offline",
|
||||
@ -744,6 +752,30 @@
|
||||
"silence": "Your account has been limited.",
|
||||
"suspend": "Your account has been suspended.",
|
||||
"learn_more": "Learn More"
|
||||
},
|
||||
"filtered_notification": {
|
||||
"title": "Filtered Notifications",
|
||||
"accept": "Accept",
|
||||
"dismiss": "Dismiss",
|
||||
},
|
||||
"policy": {
|
||||
"title": "Filter Notifications from…",
|
||||
"not_following": {
|
||||
"title": "People you don't follow",
|
||||
"subtitle": "Until you manually approve them"
|
||||
},
|
||||
"no_follower": {
|
||||
"title": "People not following you",
|
||||
"subtitle": "Including people who have been following you fewer than 3 days"
|
||||
},
|
||||
"new_account": {
|
||||
"title": "New accounts",
|
||||
"subtitle": "Created within the past 30 days"
|
||||
},
|
||||
"private_mentions": {
|
||||
"title": "Unsolicited private mentions",
|
||||
"subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
@ -755,6 +755,30 @@
|
||||
"silence": "Your account has been limited.",
|
||||
"suspend": "Your account has been suspended.",
|
||||
"learn_more": "Learn More"
|
||||
},
|
||||
"filtered_notification": {
|
||||
"title": "Filtered Notifications",
|
||||
"accept": "Accept",
|
||||
"dismiss": "Dismiss",
|
||||
},
|
||||
"policy": {
|
||||
"title": "Filter Notifications from…",
|
||||
"not_following": {
|
||||
"title": "People you don't follow",
|
||||
"subtitle": "Until you manually approve them"
|
||||
},
|
||||
"no_follower": {
|
||||
"title": "People not following you",
|
||||
"subtitle": "Including people who have been following you fewer than 3 days"
|
||||
},
|
||||
"new_account": {
|
||||
"title": "New accounts",
|
||||
"subtitle": "Created within the past 30 days"
|
||||
},
|
||||
"private_mentions": {
|
||||
"title": "Unsolicited private mentions",
|
||||
"subtitle": "Filtered unless it’s in reply to your own mention or if you follow the sender"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thread": {
|
||||
|
@ -157,10 +157,20 @@
|
||||
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 */; };
|
||||
D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */; };
|
||||
D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */; };
|
||||
D84FA0932AE6915800987F47 /* MBProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = D84FA0922AE6915800987F47 /* MBProgressHUD */; };
|
||||
D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */; };
|
||||
D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */; };
|
||||
D85DF96B2C481AF700A01408 /* NotificationPolicyFilterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9682C481AF400A01408 /* NotificationPolicyFilterTableViewCell.swift */; };
|
||||
D85DF96C2C481AF700A01408 /* NotificationPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9692C481AF700A01408 /* NotificationPolicyViewController.swift */; };
|
||||
D85DF96D2C481AF700A01408 /* NotificationPolicyHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF96A2C481AF700A01408 /* NotificationPolicyHeaderView.swift */; };
|
||||
D85DF9712C481B1100A01408 /* NotificationRequestsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF96E2C481B1100A01408 /* NotificationRequestsTableViewController.swift */; };
|
||||
D85DF9722C481B1100A01408 /* NotificationRequestTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF96F2C481B1100A01408 /* NotificationRequestTableViewCell.swift */; };
|
||||
D85DF9742C481B3500A01408 /* DataSourceFacade+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */; };
|
||||
D85DF9762C4965A900A01408 /* NotificationRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */; };
|
||||
D85DF97A2C4E49A400A01408 /* NotificationRequestCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */; };
|
||||
D85DF97E2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */; };
|
||||
D87364F92AE28DB500C8F919 /* Kanna in Frameworks */ = {isa = PBXBuildFile; productRef = D87364F82AE28DB500C8F919 /* Kanna */; };
|
||||
D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; };
|
||||
D87BFC8D291EB81200FEE264 /* MastodonLoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */; };
|
||||
@ -789,6 +799,7 @@
|
||||
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; };
|
||||
D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFilteringBannerTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = "<group>"; };
|
||||
D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = "<group>"; };
|
||||
@ -799,6 +810,15 @@
|
||||
D84C09A42B0F9E41009E685E /* Acknowledgments.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Acknowledgments.md; sourceTree = "<group>"; };
|
||||
D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstanceViewController.swift; sourceTree = "<group>"; };
|
||||
D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceRulesViewController.swift; sourceTree = "<group>"; };
|
||||
D85DF9682C481AF400A01408 /* NotificationPolicyFilterTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyFilterTableViewCell.swift; path = Policy/NotificationPolicyFilterTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D85DF9692C481AF700A01408 /* NotificationPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyViewController.swift; path = Policy/NotificationPolicyViewController.swift; sourceTree = "<group>"; };
|
||||
D85DF96A2C481AF700A01408 /* NotificationPolicyHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyHeaderView.swift; path = Policy/NotificationPolicyHeaderView.swift; sourceTree = "<group>"; };
|
||||
D85DF96E2C481B1100A01408 /* NotificationRequestsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRequestsTableViewController.swift; sourceTree = "<group>"; };
|
||||
D85DF96F2C481B1100A01408 /* NotificationRequestTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRequestTableViewCell.swift; sourceTree = "<group>"; };
|
||||
D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Notifications.swift"; sourceTree = "<group>"; };
|
||||
D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestsViewModel.swift; sourceTree = "<group>"; };
|
||||
D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestCountView.swift; sourceTree = "<group>"; };
|
||||
D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNotificationTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = "<group>"; };
|
||||
@ -1510,6 +1530,7 @@
|
||||
DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */,
|
||||
DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */,
|
||||
D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */,
|
||||
D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
@ -1758,6 +1779,16 @@
|
||||
path = Privacy;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D80EC2602C2978CB009724A5 /* Notification Filtering */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D85DF9682C481AF400A01408 /* NotificationPolicyFilterTableViewCell.swift */,
|
||||
D85DF96A2C481AF700A01408 /* NotificationPolicyHeaderView.swift */,
|
||||
D85DF9692C481AF700A01408 /* NotificationPolicyViewController.swift */,
|
||||
);
|
||||
path = "Notification Filtering";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D80F627E2B5C32E400877059 /* NotificationView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1820,6 +1851,27 @@
|
||||
path = Documentation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D85DF9702C481B1100A01408 /* Requests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D85DF97C2C50EF8700A01408 /* Account Notifications */,
|
||||
D85DF96E2C481B1100A01408 /* NotificationRequestsTableViewController.swift */,
|
||||
D85DF96F2C481B1100A01408 /* NotificationRequestTableViewCell.swift */,
|
||||
D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */,
|
||||
D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */,
|
||||
);
|
||||
name = Requests;
|
||||
path = "Notification Filtering/Requests";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D85DF97C2C50EF8700A01408 /* Account Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */,
|
||||
);
|
||||
path = "Account Notifications";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D8A6AB68291C50F3003AB663 /* Login */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2397,6 +2449,7 @@
|
||||
DB697DDC278F521D004EF2F7 /* DataSourceFacade.swift */,
|
||||
6213AF5D2893A8B200BCADB6 /* DataSourceFacade+Bookmark.swift */,
|
||||
DB697DE0278F5296004EF2F7 /* DataSourceFacade+Model.swift */,
|
||||
D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */,
|
||||
DB697DDE278F524F004EF2F7 /* DataSourceFacade+Profile.swift */,
|
||||
DB8F7075279E954700E1225B /* DataSourceFacade+Follow.swift */,
|
||||
DB603110279EB38500A935FE /* DataSourceFacade+Mute.swift */,
|
||||
@ -2667,6 +2720,8 @@
|
||||
DB9D6BFD25E4F57B0051B173 /* Notification */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D85DF9702C481B1100A01408 /* Requests */,
|
||||
D80EC2602C2978CB009724A5 /* Notification Filtering */,
|
||||
DB63F765279A5E5600455B82 /* NotificationTimeline */,
|
||||
2D35237F26256F470031AF25 /* Cell */,
|
||||
D80F627E2B5C32E400877059 /* NotificationView */,
|
||||
@ -3497,6 +3552,7 @@
|
||||
DBDFF1902805543100557A48 /* DiscoveryPostsViewController.swift in Sources */,
|
||||
DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
D85DF9712C481B1100A01408 /* NotificationRequestsTableViewController.swift in Sources */,
|
||||
D8FAAE432AD047B200DC1832 /* AboutInstanceTableFooterView.swift in Sources */,
|
||||
D808B94E296EFBBA0031EB1E /* StatusEditHistoryTableViewCell.swift in Sources */,
|
||||
D852C23E2AC5D03300309232 /* InstanceRulesViewController.swift in Sources */,
|
||||
@ -3535,6 +3591,7 @@
|
||||
DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */,
|
||||
DB5B54A62833BE0000DEF8B2 /* UserListViewModel+State.swift in Sources */,
|
||||
DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */,
|
||||
D85DF9722C481B1100A01408 /* NotificationRequestTableViewCell.swift in Sources */,
|
||||
D808B94C296ECFDC0031EB1E /* StatusEditHistoryViewModel.swift in Sources */,
|
||||
DB0617F527855AB90030EE79 /* ServerRuleSection.swift in Sources */,
|
||||
DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */,
|
||||
@ -3557,6 +3614,7 @@
|
||||
DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */,
|
||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
||||
D85DF9762C4965A900A01408 /* NotificationRequestsViewModel.swift in Sources */,
|
||||
DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */,
|
||||
DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */,
|
||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||
@ -3581,6 +3639,7 @@
|
||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||
DB5B7295273112B100081888 /* FollowingListViewController.swift in Sources */,
|
||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */,
|
||||
D85DF96C2C481AF700A01408 /* NotificationPolicyViewController.swift in Sources */,
|
||||
D81A94172B07A1D30067A19D /* ProfileCardView+Configuration.swift in Sources */,
|
||||
DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */,
|
||||
D82BD7552ABC73AF009A374A /* NotificationPolicyTableViewCell.swift in Sources */,
|
||||
@ -3601,6 +3660,7 @@
|
||||
2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */,
|
||||
DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */,
|
||||
D8E5C349296DB8A3007E76A7 /* StatusEditHistoryViewController.swift in Sources */,
|
||||
D85DF96B2C481AF700A01408 /* NotificationPolicyFilterTableViewCell.swift in Sources */,
|
||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */,
|
||||
D8318A882A4468D300C0FB73 /* NotificationSettingsViewController.swift in Sources */,
|
||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||
@ -3753,6 +3813,7 @@
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
DB0FCB822796AC78006C02E2 /* UserTimelineViewController+DataSourceProvider.swift in Sources */,
|
||||
DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */,
|
||||
D85DF96D2C481AF700A01408 /* NotificationPolicyHeaderView.swift in Sources */,
|
||||
DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */,
|
||||
2D7867192625B77500211898 /* NotificationItem.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
@ -3766,9 +3827,11 @@
|
||||
DB1D84382657B275000346B3 /* SegmentedControlNavigateable.swift in Sources */,
|
||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB7A9F932818F33C0016AF98 /* MastodonServerRulesViewController+Debug.swift in Sources */,
|
||||
D85DF97E2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift in Sources */,
|
||||
2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */,
|
||||
DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */,
|
||||
D83B54F82C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift in Sources */,
|
||||
DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */,
|
||||
D84738D42BBD9ABE00ECD52B /* TimelineStatusPill.swift in Sources */,
|
||||
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */,
|
||||
@ -3777,6 +3840,7 @@
|
||||
D852C23C2AC5D02C00309232 /* AboutInstanceViewController.swift in Sources */,
|
||||
D8F917142A4D74C3008A5370 /* GeneralSettingsDiffableTableViewDataSource.swift in Sources */,
|
||||
2A5242772C199EC2005B9E22 /* PrivacySafetySettingPreset.swift in Sources */,
|
||||
D85DF97A2C4E49A400A01408 /* NotificationRequestCountView.swift in Sources */,
|
||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */,
|
||||
DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */,
|
||||
@ -3854,6 +3918,7 @@
|
||||
D886FBD329DF710F00272017 /* WelcomeSeparatorView.swift in Sources */,
|
||||
DB0617FD27855BFE0030EE79 /* ServerRuleItem.swift in Sources */,
|
||||
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||
D85DF9742C481B3500A01408 /* DataSourceFacade+Notifications.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -205,7 +205,12 @@ extension SceneCoordinator {
|
||||
|
||||
// setting
|
||||
case settings(setting: Setting)
|
||||
|
||||
|
||||
// Notifications
|
||||
case notificationPolicy(viewModel: NotificationFilterViewModel)
|
||||
case notificationRequests(viewModel: NotificationRequestsViewModel)
|
||||
case accountNotificationTimeline(viewModel: NotificationTimelineViewModel, request: Mastodon.Entity.NotificationRequest)
|
||||
|
||||
// report
|
||||
case report(viewModel: ReportViewModel)
|
||||
case reportServerRules(viewModel: ReportServerRulesViewModel)
|
||||
@ -558,6 +563,12 @@ private extension SceneCoordinator {
|
||||
case .editStatus(let viewModel):
|
||||
let composeViewController = ComposeViewController(viewModel: viewModel)
|
||||
viewController = composeViewController
|
||||
case .notificationRequests(let viewModel):
|
||||
viewController = NotificationRequestsTableViewController(viewModel: viewModel)
|
||||
case .notificationPolicy(let viewModel):
|
||||
viewController = NotificationPolicyViewController(viewModel: viewModel)
|
||||
case .accountNotificationTimeline(let viewModel, let request):
|
||||
viewController = AccountNotificationTimelineViewController(viewModel: viewModel, context: appContext, coordinator: self, notificationRequest: request)
|
||||
}
|
||||
|
||||
setupDependency(for: viewController as? NeedsDependency)
|
||||
|
@ -0,0 +1,52 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
extension DataSourceFacade {
|
||||
@MainActor
|
||||
static func coordinateToNotificationRequests(
|
||||
provider: DataSourceProvider & AuthContextProvider
|
||||
) async {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
do {
|
||||
let notificationRequests = try await provider.context.apiService.notificationRequests(authenticationBox: provider.authContext.mastodonAuthenticationBox).value
|
||||
let viewModel = NotificationRequestsViewModel(appContext: provider.context, authContext: provider.authContext, coordinator: provider.coordinator, requests: notificationRequests)
|
||||
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
let transition: SceneCoordinator.Transition
|
||||
|
||||
if provider.traitCollection.userInterfaceIdiom == .phone {
|
||||
transition = .show
|
||||
} else {
|
||||
transition = .modal(animated: true)
|
||||
}
|
||||
|
||||
provider.coordinator.present(scene: .notificationRequests(viewModel: viewModel), transition: transition)
|
||||
} catch {
|
||||
//TODO: Error Handling
|
||||
provider.coordinator.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func coordinateToNotificationRequest(
|
||||
request: Mastodon.Entity.NotificationRequest,
|
||||
provider: ViewControllerWithDependencies & AuthContextProvider
|
||||
) async -> AccountNotificationTimelineViewController? {
|
||||
provider.coordinator.showLoading()
|
||||
|
||||
let notificationTimelineViewModel = NotificationTimelineViewModel(context: provider.context, authContext: provider.authContext, scope: .fromAccount(request.account))
|
||||
|
||||
provider.coordinator.hideLoading()
|
||||
|
||||
guard let viewController = provider.coordinator.present(scene: .accountNotificationTimeline(viewModel: notificationTimelineViewModel, request: request), transition: .show) as? AccountNotificationTimelineViewController else { return nil }
|
||||
|
||||
return viewController
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -17,32 +17,30 @@ extension DataSourceFacade {
|
||||
item: DataSourceItem
|
||||
) async {
|
||||
switch item {
|
||||
case .account(account: let account, relationship: _):
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: account,
|
||||
hashtag: nil
|
||||
)
|
||||
case .account(account: let account, relationship: _):
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: account,
|
||||
hashtag: nil
|
||||
)
|
||||
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .hashtag(let tag):
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .hashtag(let tag):
|
||||
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: nil,
|
||||
hashtag: tag
|
||||
)
|
||||
let now = Date()
|
||||
let userID = provider.authContext.mastodonAuthenticationBox.userID
|
||||
let searchEntry = Persistence.SearchHistory.Item(
|
||||
updatedAt: now,
|
||||
userID: userID,
|
||||
account: nil,
|
||||
hashtag: tag
|
||||
)
|
||||
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .status:
|
||||
break
|
||||
case .notification:
|
||||
try? FileManager.default.addSearchItem(searchEntry, for: provider.authContext.mastodonAuthenticationBox)
|
||||
case .status, .notification, .notificationBanner(_):
|
||||
break
|
||||
|
||||
}
|
||||
|
@ -514,10 +514,9 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider & Aut
|
||||
)
|
||||
case .account(let account, _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .notification:
|
||||
assertionFailure("TODO")
|
||||
case .hashtag(_):
|
||||
assertionFailure("TODO")
|
||||
case .notification, .hashtag(_), .notificationBanner(_):
|
||||
// not supposed to happen
|
||||
break
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
|
@ -618,10 +618,9 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte
|
||||
provider: self,
|
||||
account: account
|
||||
)
|
||||
case .notification:
|
||||
assertionFailure("TODO")
|
||||
case .hashtag(_):
|
||||
assertionFailure("TODO")
|
||||
case .notification, .hashtag(_), .notificationBanner(_):
|
||||
// not supposed to happen
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,42 +22,44 @@ extension UITableViewDelegate where Self: DataSourceProvider & AuthContextProvid
|
||||
return
|
||||
}
|
||||
switch item {
|
||||
case .account(let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .status(let status):
|
||||
case .account(let account, relationship: _):
|
||||
await DataSourceFacade.coordinateToProfileScene(provider: self, account: account)
|
||||
case .status(let status):
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: tag
|
||||
)
|
||||
case .notification(let notification):
|
||||
let _status: MastodonStatus? = notification.status
|
||||
if let status = _status {
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
case .hashtag(let tag):
|
||||
await DataSourceFacade.coordinateToHashtagScene(
|
||||
provider: self,
|
||||
tag: tag
|
||||
} else if let accountWarning = notification.entity.accountWarning {
|
||||
let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id)
|
||||
_ = coordinator.present(
|
||||
scene: .safari(url: url),
|
||||
from: self,
|
||||
transition: .safariPresent(animated: true, completion: nil)
|
||||
)
|
||||
case .notification(let notification):
|
||||
let _status: MastodonStatus? = notification.status
|
||||
if let status = _status {
|
||||
await DataSourceFacade.coordinateToStatusThreadScene(
|
||||
provider: self,
|
||||
target: .status, // remove reblog wrapper
|
||||
status: status
|
||||
)
|
||||
} else if let accountWarning = notification.entity.accountWarning {
|
||||
let url = Mastodon.API.disputesEndpoint(domain: authContext.mastodonAuthenticationBox.domain, strikeId: accountWarning.id)
|
||||
_ = coordinator.present(
|
||||
scene: .safari(url: url),
|
||||
from: self,
|
||||
transition: .safariPresent(animated: true, completion: nil)
|
||||
)
|
||||
|
||||
} else {
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
account: notification.entity.account
|
||||
)
|
||||
} // end Task
|
||||
} // end func
|
||||
} else {
|
||||
await DataSourceFacade.coordinateToProfileScene(
|
||||
provider: self,
|
||||
account: notification.entity.account
|
||||
)
|
||||
}
|
||||
case .notificationBanner(let policy):
|
||||
await DataSourceFacade.coordinateToNotificationRequests(provider: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ enum DataSourceItem: Hashable {
|
||||
case status(record: MastodonStatus)
|
||||
case hashtag(tag: Mastodon.Entity.Tag)
|
||||
case notification(record: MastodonNotification)
|
||||
case notificationBanner(policy: Mastodon.Entity.NotificationPolicy)
|
||||
case account(account: Mastodon.Entity.Account, relationship: Mastodon.Entity.Relationship?)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,88 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
class NotificationFilteringBannerTableViewCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "NotificationFilteringBannerTableViewCell"
|
||||
|
||||
let iconImageView: UIImageView
|
||||
let iconImageWrapperView: UIView
|
||||
|
||||
let titleLabel: UILabel
|
||||
let subtitleLabel: UILabel
|
||||
private let contentStackView: UIStackView
|
||||
private let labelStackView: UIStackView
|
||||
let separatorLine: UIView
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
|
||||
let iconConfiguration = UIImage.SymbolConfiguration(scale: .large)
|
||||
let icon = UIImage(systemName: "archivebox", withConfiguration: iconConfiguration)
|
||||
iconImageView = UIImageView(image: icon)
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
iconImageWrapperView = UIView()
|
||||
iconImageWrapperView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconImageWrapperView.addSubview(iconImageView)
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.text = L10n.Scene.Notification.FilteredNotification.title
|
||||
titleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
subtitleLabel.textColor = .secondaryLabel
|
||||
|
||||
labelStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
labelStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
labelStackView.alignment = .leading
|
||||
labelStackView.axis = .vertical
|
||||
|
||||
contentStackView = UIStackView(arrangedSubviews: [iconImageWrapperView, labelStackView])
|
||||
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentStackView.alignment = .center
|
||||
contentStackView.axis = .horizontal
|
||||
contentStackView.spacing = 12
|
||||
|
||||
separatorLine = UIView.separatorLine
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(contentStackView)
|
||||
contentView.addSubview(separatorLine)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
iconImageWrapperView.widthAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.width),
|
||||
iconImageWrapperView.heightAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.height).priority(.defaultHigh),
|
||||
iconImageView.centerXAnchor.constraint(equalTo: iconImageWrapperView.centerXAnchor),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: iconImageWrapperView.centerYAnchor),
|
||||
|
||||
contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
contentView.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor, constant: 16),
|
||||
separatorLine.topAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 7),
|
||||
|
||||
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView))
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
func configure(with policy: Mastodon.Entity.NotificationPolicy) {
|
||||
subtitleLabel.text = L10n.Plural.FilteredNotificationBanner.subtitle(policy.summary.pendingRequestsCount)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol NotificationPolicyFilterTableViewCellDelegate: AnyObject {
|
||||
func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool)
|
||||
}
|
||||
|
||||
class NotificationPolicyFilterTableViewCell: ToggleTableViewCell {
|
||||
override class var reuseIdentifier: String {
|
||||
return "NotificationPolicyFilterTableViewCell"
|
||||
}
|
||||
|
||||
var filterItem: NotificationFilterItem?
|
||||
weak var delegate: NotificationPolicyFilterTableViewCellDelegate?
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
subtitleLabel.textColor = .secondaryLabel
|
||||
subtitleLabel.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
|
||||
toggle.addTarget(self, action: #selector(NotificationPolicyFilterTableViewCell.toggleValueChanged(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
public func configure(with filterItem: NotificationFilterItem, viewModel: NotificationFilterViewModel) {
|
||||
label.text = filterItem.title
|
||||
subtitleLabel.text = filterItem.subtitle
|
||||
self.filterItem = filterItem
|
||||
|
||||
let toggleIsOn: Bool
|
||||
switch filterItem {
|
||||
case .notFollowing:
|
||||
toggleIsOn = viewModel.notFollowing
|
||||
case .noFollower:
|
||||
toggleIsOn = viewModel.noFollower
|
||||
case .newAccount:
|
||||
toggleIsOn = viewModel.newAccount
|
||||
case .privateMentions:
|
||||
toggleIsOn = viewModel.privateMentions
|
||||
}
|
||||
|
||||
toggle.isOn = toggleIsOn
|
||||
}
|
||||
|
||||
@objc func toggleValueChanged(_ sender: UISwitch) {
|
||||
guard let filterItem, let delegate else { return }
|
||||
|
||||
delegate.toggleValueChanged(self, filterItem: filterItem, newValue: sender.isOn)
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonLocalization
|
||||
|
||||
class NotificationPolicyHeaderView: UIView {
|
||||
let titleLabel: UILabel
|
||||
let closeButton: UIButton
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleLabel.font = UIFontMetrics(forTextStyle: .title3).scaledFont(for: .systemFont(ofSize: 20, weight: .bold))
|
||||
titleLabel.text = L10n.Scene.Notification.Policy.title
|
||||
|
||||
|
||||
let buttonImageConfiguration = UIImage
|
||||
.SymbolConfiguration(pointSize: 30)
|
||||
.applying(UIImage.SymbolConfiguration(paletteColors: [.secondaryLabel, .quaternarySystemFill]))
|
||||
let buttonImage = UIImage(systemName: "xmark.circle.fill", withConfiguration: buttonImageConfiguration)
|
||||
var buttonConfiguration = UIButton.Configuration.plain()
|
||||
buttonConfiguration.image = buttonImage
|
||||
buttonConfiguration.contentInsets = .init(top: 0, leading: 10, bottom: 0, trailing: 0)
|
||||
|
||||
closeButton = UIButton(configuration: buttonConfiguration)
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButton.contentMode = .center
|
||||
|
||||
super.init(frame: frame)
|
||||
addSubview(titleLabel)
|
||||
addSubview(closeButton)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
|
||||
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
|
||||
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor, constant: 8),
|
||||
bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor),
|
||||
|
||||
closeButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
|
||||
trailingAnchor.constraint(equalTo: closeButton.trailingAnchor, constant: 20),
|
||||
bottomAnchor.constraint(greaterThanOrEqualTo: closeButton.bottomAnchor)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
enum NotificationFilterSection: Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
enum NotificationFilterItem: Hashable, CaseIterable {
|
||||
case notFollowing
|
||||
case noFollower
|
||||
case newAccount
|
||||
case privateMentions
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .notFollowing:
|
||||
return L10n.Scene.Notification.Policy.NotFollowing.title
|
||||
case .noFollower:
|
||||
return L10n.Scene.Notification.Policy.NoFollower.title
|
||||
case .newAccount:
|
||||
return L10n.Scene.Notification.Policy.NewAccount.title
|
||||
case .privateMentions:
|
||||
return L10n.Scene.Notification.Policy.PrivateMentions.title
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
switch self {
|
||||
case .notFollowing:
|
||||
return L10n.Scene.Notification.Policy.NotFollowing.subtitle
|
||||
case .noFollower:
|
||||
return L10n.Scene.Notification.Policy.NoFollower.subtitle
|
||||
case .newAccount:
|
||||
return L10n.Scene.Notification.Policy.NewAccount.subtitle
|
||||
case .privateMentions:
|
||||
return L10n.Scene.Notification.Policy.PrivateMentions.subtitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationFilterViewModel {
|
||||
var notFollowing: Bool
|
||||
var noFollower: Bool
|
||||
var newAccount: Bool
|
||||
var privateMentions: Bool
|
||||
|
||||
let appContext: AppContext
|
||||
|
||||
init(appContext: AppContext, notFollowing: Bool, noFollower: Bool, newAccount: Bool, privateMentions: Bool) {
|
||||
self.appContext = appContext
|
||||
self.notFollowing = notFollowing
|
||||
self.noFollower = noFollower
|
||||
self.newAccount = newAccount
|
||||
self.privateMentions = privateMentions
|
||||
}
|
||||
}
|
||||
|
||||
protocol NotificationPolicyViewControllerDelegate: AnyObject {
|
||||
func policyUpdated(_ viewController: NotificationPolicyViewController, newPolicy: Mastodon.Entity.NotificationPolicy)
|
||||
}
|
||||
|
||||
class NotificationPolicyViewController: UIViewController {
|
||||
|
||||
let tableView: UITableView
|
||||
let headerBar: NotificationPolicyHeaderView
|
||||
var saveItem: UIBarButtonItem?
|
||||
var dataSource: UITableViewDiffableDataSource<NotificationFilterSection, NotificationFilterItem>?
|
||||
let items: [NotificationFilterItem]
|
||||
var viewModel: NotificationFilterViewModel
|
||||
weak var delegate: NotificationPolicyViewControllerDelegate?
|
||||
|
||||
init(viewModel: NotificationFilterViewModel) {
|
||||
self.viewModel = viewModel
|
||||
items = NotificationFilterItem.allCases
|
||||
|
||||
headerBar = NotificationPolicyHeaderView()
|
||||
headerBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.register(NotificationPolicyFilterTableViewCell.self, forCellReuseIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier)
|
||||
tableView.contentInset.top = -20
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<NotificationFilterSection, NotificationFilterItem>(tableView: tableView) { [weak self] tableView, indexPath, itemIdentifier in
|
||||
guard let self, let cell = tableView.dequeueReusableCell(withIdentifier: NotificationPolicyFilterTableViewCell.reuseIdentifier, for: indexPath) as? NotificationPolicyFilterTableViewCell else {
|
||||
fatalError("No NotificationPolicyFilterTableViewCell")
|
||||
}
|
||||
|
||||
let item = items[indexPath.row]
|
||||
cell.configure(with: item, viewModel: self.viewModel)
|
||||
cell.delegate = self
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
tableView.dataSource = dataSource
|
||||
tableView.delegate = self
|
||||
|
||||
self.dataSource = dataSource
|
||||
view.addSubview(headerBar)
|
||||
view.addSubview(tableView)
|
||||
view.backgroundColor = .systemGroupedBackground
|
||||
headerBar.closeButton.addTarget(self, action: #selector(NotificationPolicyViewController.save(_:)), for: .touchUpInside)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationFilterSection, NotificationFilterItem>()
|
||||
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(items)
|
||||
|
||||
dataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
headerBar.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
headerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: headerBar.trailingAnchor),
|
||||
|
||||
tableView.topAnchor.constraint(equalTo: headerBar.bottomAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor),
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
@objc private func save(_ sender: UIButton) {
|
||||
guard let authenticationBox = viewModel.appContext.authenticationService.mastodonAuthenticationBoxes.first else { return }
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
do {
|
||||
let updatedPolicy = try await viewModel.appContext.apiService.updateNotificationPolicy(
|
||||
authenticationBox: authenticationBox,
|
||||
filterNotFollowing: viewModel.notFollowing,
|
||||
filterNotFollowers: viewModel.noFollower,
|
||||
filterNewAccounts: viewModel.newAccount,
|
||||
filterPrivateMentions: viewModel.privateMentions
|
||||
).value
|
||||
|
||||
delegate?.policyUpdated(self, newPolicy: updatedPolicy)
|
||||
|
||||
NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil)
|
||||
|
||||
} catch {}
|
||||
}
|
||||
|
||||
dismiss(animated:true)
|
||||
}
|
||||
|
||||
@objc private func cancel(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UITableViewDelegate
|
||||
|
||||
extension NotificationPolicyViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let filterItem = items[indexPath.row]
|
||||
switch filterItem {
|
||||
case .notFollowing:
|
||||
viewModel.notFollowing.toggle()
|
||||
case .noFollower:
|
||||
viewModel.noFollower.toggle()
|
||||
case .newAccount:
|
||||
viewModel.newAccount.toggle()
|
||||
case .privateMentions:
|
||||
viewModel.privateMentions.toggle()
|
||||
}
|
||||
|
||||
if let snapshot = dataSource?.snapshot() {
|
||||
dataSource?.applySnapshotUsingReloadData(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationPolicyViewController: NotificationPolicyFilterTableViewCellDelegate {
|
||||
func toggleValueChanged(_ tableViewCell: NotificationPolicyFilterTableViewCell, filterItem: NotificationFilterItem, newValue: Bool) {
|
||||
switch filterItem {
|
||||
case .notFollowing:
|
||||
viewModel.notFollowing = newValue
|
||||
case .noFollower:
|
||||
viewModel.noFollower = newValue
|
||||
case .newAccount:
|
||||
viewModel.newAccount = newValue
|
||||
case .privateMentions:
|
||||
viewModel.privateMentions = newValue
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
import MastodonLocalization
|
||||
|
||||
protocol AccountNotificationTimelineViewControllerDelegate: AnyObject {
|
||||
func acceptRequest(_ viewController: AccountNotificationTimelineViewController, request: Mastodon.Entity.NotificationRequest)
|
||||
func dismissRequest(_ viewController: AccountNotificationTimelineViewController, request: Mastodon.Entity.NotificationRequest)
|
||||
}
|
||||
|
||||
class AccountNotificationTimelineViewController: NotificationTimelineViewController {
|
||||
|
||||
let request: Mastodon.Entity.NotificationRequest
|
||||
weak var delegate: AccountNotificationTimelineViewControllerDelegate?
|
||||
|
||||
init(viewModel: NotificationTimelineViewModel, context: AppContext, coordinator: SceneCoordinator, notificationRequest: Mastodon.Entity.NotificationRequest) {
|
||||
self.request = notificationRequest
|
||||
|
||||
super.init(viewModel: viewModel, context: context, coordinator: coordinator)
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(title: nil, image: UIImage(systemName: "ellipsis.circle"), target: nil, action: nil, menu: menu())
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func menu() -> UIMenu {
|
||||
let menu = UIMenu(children: [
|
||||
UIAction(title: L10n.Scene.Notification.FilteredNotification.accept, image: UIImage(systemName: "checkmark")) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
coordinator.showLoading()
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
self.delegate?.acceptRequest(self, request: request)
|
||||
coordinator.hideLoading()
|
||||
},
|
||||
UIAction(title: L10n.Scene.Notification.FilteredNotification.dismiss, image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
coordinator.showLoading()
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
self.delegate?.dismissRequest(self, request: request)
|
||||
coordinator.hideLoading()
|
||||
}
|
||||
])
|
||||
|
||||
return menu
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
|
||||
class NotificationRequestCountView: UIView {
|
||||
|
||||
let countLabel: UILabel
|
||||
|
||||
init() {
|
||||
countLabel = UILabel()
|
||||
countLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
countLabel.textColor = .white
|
||||
countLabel.font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .regular))
|
||||
countLabel.textAlignment = .center
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(countLabel)
|
||||
|
||||
backgroundColor = Asset.Colors.Brand.blurple.color
|
||||
layer.borderWidth = 2.0
|
||||
layer.borderColor = UIColor.white.cgColor
|
||||
applyCornerRadius(radius: 10)
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
countLabel.topAnchor.constraint(equalTo: topAnchor, constant: 2),
|
||||
countLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5),
|
||||
trailingAnchor.constraint(equalTo: countLabel.trailingAnchor, constant: 5),
|
||||
bottomAnchor.constraint(equalTo: countLabel.bottomAnchor, constant: 2),
|
||||
|
||||
widthAnchor.constraint(greaterThanOrEqualToConstant: 20),
|
||||
heightAnchor.constraint(greaterThanOrEqualToConstant: 20)
|
||||
]
|
||||
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MetaTextKit
|
||||
import MastodonMeta
|
||||
import MastodonUI
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonAsset
|
||||
|
||||
protocol NotificationRequestTableViewCellDelegate: AnyObject {
|
||||
func acceptNotificationRequest(_ cell: NotificationRequestTableViewCell, notificationRequest: Mastodon.Entity.NotificationRequest)
|
||||
func rejectNotificationRequest(_ cell: NotificationRequestTableViewCell, notificationRequest: Mastodon.Entity.NotificationRequest)
|
||||
}
|
||||
|
||||
class NotificationRequestTableViewCell: UITableViewCell {
|
||||
static let reuseIdentifier = "NotificationRequestTableViewCell"
|
||||
|
||||
var notificationRequest: Mastodon.Entity.NotificationRequest?
|
||||
weak var delegate: NotificationRequestTableViewCellDelegate?
|
||||
|
||||
let nameLabel: MetaLabel
|
||||
let usernameLabel: MetaLabel
|
||||
let avatarButton: AvatarButton
|
||||
let chevronImageView: UIImageView
|
||||
|
||||
private let labelStackView: UIStackView
|
||||
private let avatarStackView: UIStackView
|
||||
private let contentStackView: UIStackView
|
||||
|
||||
let acceptNotificationRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer()
|
||||
let acceptNotificationRequestButton: HighlightDimmableButton
|
||||
let acceptNotificationRequestActivityIndicatorView: UIActivityIndicatorView
|
||||
|
||||
let rejectNotificationRequestButtonShadowBackgroundContainer = ShadowBackgroundContainer()
|
||||
let rejectNotificationRequestActivityIndicatorView: UIActivityIndicatorView
|
||||
let rejectNotificationRequestButton: HighlightDimmableButton
|
||||
|
||||
let requestCountView: NotificationRequestCountView
|
||||
|
||||
private let buttonStackView: UIStackView
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
nameLabel = MetaLabel(style: .statusName)
|
||||
usernameLabel = MetaLabel(style: .statusUsername)
|
||||
avatarButton = AvatarButton()
|
||||
avatarButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarButton.size = CGSize.authorAvatarButtonSize
|
||||
avatarButton.avatarImageView.imageViewSize = CGSize.authorAvatarButtonSize
|
||||
|
||||
labelStackView = UIStackView(arrangedSubviews: [nameLabel, usernameLabel])
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.alignment = .leading
|
||||
labelStackView.spacing = 4
|
||||
|
||||
acceptNotificationRequestButton = HighlightDimmableButton()
|
||||
acceptNotificationRequestButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
acceptNotificationRequestButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
acceptNotificationRequestButton.setTitleColor(.white, for: .normal)
|
||||
acceptNotificationRequestButton.setTitle(L10n.Scene.Notification.FilteredNotification.accept, for: .normal)
|
||||
acceptNotificationRequestButton.setImage(UIImage(systemName: "checkmark"), for: .normal)
|
||||
acceptNotificationRequestButton.imageView?.contentMode = .scaleAspectFit
|
||||
acceptNotificationRequestButton.setBackgroundImage(.placeholder(color: Asset.Scene.Notification.confirmFollowRequestButtonBackground.color), for: .normal)
|
||||
acceptNotificationRequestButton.setInsets(forContentPadding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), imageTitlePadding: 8)
|
||||
acceptNotificationRequestButton.tintColor = .white
|
||||
acceptNotificationRequestButton.layer.masksToBounds = true
|
||||
acceptNotificationRequestButton.layer.cornerCurve = .continuous
|
||||
acceptNotificationRequestButton.layer.cornerRadius = 10
|
||||
acceptNotificationRequestButton.accessibilityLabel = L10n.Scene.Notification.FollowRequest.accept
|
||||
acceptNotificationRequestButtonShadowBackgroundContainer.cornerRadius = 10
|
||||
acceptNotificationRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1
|
||||
acceptNotificationRequestButtonShadowBackgroundContainer.addSubview(acceptNotificationRequestButton)
|
||||
|
||||
acceptNotificationRequestActivityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
acceptNotificationRequestActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
acceptNotificationRequestActivityIndicatorView.color = .white
|
||||
acceptNotificationRequestActivityIndicatorView.hidesWhenStopped = true
|
||||
acceptNotificationRequestActivityIndicatorView.stopAnimating()
|
||||
acceptNotificationRequestButton.addSubview(acceptNotificationRequestActivityIndicatorView)
|
||||
|
||||
rejectNotificationRequestButton = HighlightDimmableButton()
|
||||
rejectNotificationRequestButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
rejectNotificationRequestButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
rejectNotificationRequestButton.setTitleColor(.black, for: .normal)
|
||||
rejectNotificationRequestButton.setTitle(L10n.Scene.Notification.FilteredNotification.dismiss, for: .normal)
|
||||
rejectNotificationRequestButton.setImage(UIImage(systemName: "speaker.slash"), for: .normal)
|
||||
rejectNotificationRequestButton.imageView?.contentMode = .scaleAspectFit
|
||||
rejectNotificationRequestButton.setInsets(forContentPadding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), imageTitlePadding: 8)
|
||||
rejectNotificationRequestButton.setBackgroundImage(.placeholder(color: Asset.Scene.Notification.deleteFollowRequestButtonBackground.color), for: .normal)
|
||||
rejectNotificationRequestButton.tintColor = .black
|
||||
rejectNotificationRequestButton.layer.masksToBounds = true
|
||||
rejectNotificationRequestButton.layer.cornerCurve = .continuous
|
||||
rejectNotificationRequestButton.layer.cornerRadius = 10
|
||||
rejectNotificationRequestButton.accessibilityLabel = L10n.Scene.Notification.FollowRequest.reject
|
||||
rejectNotificationRequestButtonShadowBackgroundContainer.cornerRadius = 10
|
||||
rejectNotificationRequestButtonShadowBackgroundContainer.shadowAlpha = 0.1
|
||||
rejectNotificationRequestButtonShadowBackgroundContainer.addSubview(rejectNotificationRequestButton)
|
||||
|
||||
rejectNotificationRequestActivityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
rejectNotificationRequestActivityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
rejectNotificationRequestActivityIndicatorView.color = .black
|
||||
rejectNotificationRequestActivityIndicatorView.hidesWhenStopped = true
|
||||
rejectNotificationRequestActivityIndicatorView.stopAnimating()
|
||||
rejectNotificationRequestButton.addSubview(rejectNotificationRequestActivityIndicatorView)
|
||||
|
||||
buttonStackView = UIStackView(arrangedSubviews: [rejectNotificationRequestButtonShadowBackgroundContainer, acceptNotificationRequestButtonShadowBackgroundContainer])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.distribution = .fillEqually
|
||||
buttonStackView.spacing = 16
|
||||
buttonStackView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0) // set bottom padding
|
||||
|
||||
chevronImageView = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||
chevronImageView.tintColor = .tertiaryLabel
|
||||
|
||||
avatarStackView = UIStackView(arrangedSubviews: [avatarButton, labelStackView, UIView(), chevronImageView])
|
||||
avatarStackView.axis = .horizontal
|
||||
avatarStackView.alignment = .center
|
||||
avatarStackView.spacing = 12
|
||||
|
||||
contentStackView = UIStackView(arrangedSubviews: [avatarStackView, buttonStackView])
|
||||
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentStackView.spacing = 16
|
||||
contentStackView.axis = .vertical
|
||||
contentStackView.alignment = .leading
|
||||
|
||||
requestCountView = NotificationRequestCountView()
|
||||
requestCountView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
acceptNotificationRequestButton.addTarget(self, action: #selector(NotificationRequestTableViewCell.acceptNotificationRequest(_:)), for: .touchUpInside)
|
||||
rejectNotificationRequestButton.addTarget(self, action: #selector(NotificationRequestTableViewCell.rejectNotificationRequest(_:)), for: .touchUpInside)
|
||||
|
||||
contentView.addSubview(contentStackView)
|
||||
contentView.addSubview(requestCountView)
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func setupConstraints() {
|
||||
let constraints = [
|
||||
|
||||
contentStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
|
||||
contentStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
contentView.trailingAnchor.constraint(equalTo: contentStackView.trailingAnchor, constant: 16),
|
||||
contentView.bottomAnchor.constraint(equalTo: contentStackView.bottomAnchor, constant: 16),
|
||||
|
||||
buttonStackView.widthAnchor.constraint(equalTo: contentStackView.widthAnchor),
|
||||
avatarStackView.widthAnchor.constraint(equalTo: contentStackView.widthAnchor),
|
||||
|
||||
avatarButton.widthAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.width).priority(.required - 1),
|
||||
avatarButton.heightAnchor.constraint(equalToConstant: CGSize.authorAvatarButtonSize.height).priority(.required - 1),
|
||||
|
||||
acceptNotificationRequestActivityIndicatorView.centerXAnchor.constraint(equalTo: acceptNotificationRequestButton.centerXAnchor),
|
||||
acceptNotificationRequestActivityIndicatorView.centerYAnchor.constraint(equalTo: acceptNotificationRequestButton.centerYAnchor),
|
||||
rejectNotificationRequestActivityIndicatorView.centerXAnchor.constraint(equalTo: rejectNotificationRequestButton.centerXAnchor),
|
||||
rejectNotificationRequestActivityIndicatorView.centerYAnchor.constraint(equalTo: rejectNotificationRequestButton.centerYAnchor),
|
||||
|
||||
requestCountView.trailingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 2),
|
||||
requestCountView.bottomAnchor.constraint(equalTo: avatarButton.bottomAnchor, constant: 2),
|
||||
|
||||
]
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
|
||||
acceptNotificationRequestButton.pinToParent()
|
||||
rejectNotificationRequestButton.pinToParent()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
avatarButton.avatarImageView.image = nil
|
||||
avatarButton.avatarImageView.cancelTask()
|
||||
}
|
||||
|
||||
func configure(with request: Mastodon.Entity.NotificationRequest) {
|
||||
let account = request.account
|
||||
|
||||
avatarButton.avatarImageView.configure(with: account.avatarImageURL())
|
||||
avatarButton.avatarImageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 12)))
|
||||
|
||||
// author name
|
||||
let metaAccountName: MetaContent
|
||||
do {
|
||||
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojis.asDictionary)
|
||||
metaAccountName = try MastodonMetaContent.convert(document: content)
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
metaAccountName = PlaintextMetaContent(string: account.displayNameWithFallback)
|
||||
}
|
||||
nameLabel.configure(content: metaAccountName)
|
||||
|
||||
let metaUsername = PlaintextMetaContent(string: "@\(account.acct)")
|
||||
usernameLabel.configure(content: metaUsername)
|
||||
|
||||
requestCountView.countLabel.text = request.notificationsCount
|
||||
requestCountView.setNeedsLayout()
|
||||
requestCountView.layoutIfNeeded()
|
||||
|
||||
self.notificationRequest = request
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@objc private func acceptNotificationRequest(_ sender: UIButton) {
|
||||
guard let notificationRequest, let delegate else { return }
|
||||
|
||||
delegate.acceptNotificationRequest(self, notificationRequest: notificationRequest)
|
||||
}
|
||||
@objc private func rejectNotificationRequest(_ sender: UIButton) {
|
||||
guard let notificationRequest, let delegate else { return }
|
||||
|
||||
delegate.rejectNotificationRequest(self, notificationRequest: notificationRequest)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
enum NotificationRequestsSection: Hashable {
|
||||
case main
|
||||
}
|
||||
|
||||
enum NotificationRequestItem: Hashable {
|
||||
case item(Mastodon.Entity.NotificationRequest)
|
||||
}
|
||||
|
||||
protocol NotificationRequestsTableViewControllerDelegate: AnyObject {
|
||||
func notificationRequestsUpdated(_ viewController: NotificationRequestsTableViewController)
|
||||
}
|
||||
|
||||
class NotificationRequestsTableViewController: UIViewController, NeedsDependency {
|
||||
var context: AppContext!
|
||||
var coordinator: SceneCoordinator!
|
||||
weak var delegate: NotificationRequestsTableViewControllerDelegate?
|
||||
|
||||
let tableView: UITableView
|
||||
var viewModel: NotificationRequestsViewModel
|
||||
var dataSource: UITableViewDiffableDataSource<NotificationRequestsSection, NotificationRequestItem>?
|
||||
|
||||
init(viewModel: NotificationRequestsViewModel) {
|
||||
|
||||
self.viewModel = viewModel
|
||||
self.context = viewModel.appContext
|
||||
self.coordinator = viewModel.coordinator
|
||||
|
||||
tableView = UITableView(frame: .zero)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.backgroundColor = .secondarySystemBackground
|
||||
tableView.register(NotificationRequestTableViewCell.self, forCellReuseIdentifier: NotificationRequestTableViewCell.reuseIdentifier)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.pinToParent()
|
||||
|
||||
let dataSource = UITableViewDiffableDataSource<NotificationRequestsSection, NotificationRequestItem>(tableView: tableView) { tableView, indexPath, itemIdentifier in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: NotificationRequestTableViewCell.reuseIdentifier, for: indexPath) as? NotificationRequestTableViewCell else {
|
||||
fatalError("No NotificationRequestTableViewCell")
|
||||
}
|
||||
|
||||
let request = viewModel.requests[indexPath.row]
|
||||
cell.configure(with: request)
|
||||
cell.delegate = self
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
tableView.dataSource = dataSource
|
||||
tableView.delegate = self
|
||||
self.dataSource = dataSource
|
||||
|
||||
title = L10n.Scene.Notification.FilteredNotification.title
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationRequestsSection, NotificationRequestItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(viewModel.requests.compactMap { NotificationRequestItem.item($0) } )
|
||||
|
||||
dataSource?.apply(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension NotificationRequestsTableViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let request = viewModel.requests[indexPath.row]
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
let viewController = await DataSourceFacade.coordinateToNotificationRequest(request: request, provider: self)
|
||||
viewController?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let dismissAction = UIContextualAction(style: .normal, title: "Dismiss") { [weak self] action, view, completion in
|
||||
guard let request = self?.viewModel.requests[indexPath.row], let cell = tableView.cellForRow(at: indexPath) as? NotificationRequestTableViewCell else { return completion(false) }
|
||||
|
||||
self?.rejectNotificationRequest(cell, notificationRequest: request)
|
||||
completion(true)
|
||||
}
|
||||
|
||||
dismissAction.image = UIImage(systemName: "speaker.slash")
|
||||
|
||||
let swipeAction = UISwipeActionsConfiguration(actions: [dismissAction])
|
||||
swipeAction.performsFirstActionWithFullSwipe = true
|
||||
return swipeAction
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AuthContextProvider
|
||||
extension NotificationRequestsTableViewController: AuthContextProvider {
|
||||
var authContext: AuthContext { viewModel.authContext }
|
||||
}
|
||||
|
||||
extension NotificationRequestsTableViewController: NotificationRequestTableViewCellDelegate {
|
||||
func acceptNotificationRequest(_ cell: NotificationRequestTableViewCell, notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) {
|
||||
|
||||
cell.acceptNotificationRequestActivityIndicatorView.isHidden = false
|
||||
cell.acceptNotificationRequestActivityIndicatorView.startAnimating()
|
||||
cell.acceptNotificationRequestButton.tintColor = .clear
|
||||
cell.acceptNotificationRequestButton.setTitleColor(.clear, for: .normal)
|
||||
cell.rejectNotificationRequestButton.isUserInteractionEnabled = false
|
||||
cell.acceptNotificationRequestButton.isUserInteractionEnabled = false
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try await acceptNotificationRequest(notificationRequest)
|
||||
} catch {
|
||||
cell.acceptNotificationRequestActivityIndicatorView.stopAnimating()
|
||||
cell.acceptNotificationRequestButton.tintColor = .white
|
||||
cell.acceptNotificationRequestButton.setTitleColor(.white, for: .normal)
|
||||
cell.rejectNotificationRequestButton.isUserInteractionEnabled = true
|
||||
cell.acceptNotificationRequestButton.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func acceptNotificationRequest(_ notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) async throws {
|
||||
_ = try await context.apiService.acceptNotificationRequests(authenticationBox: authContext.mastodonAuthenticationBox,
|
||||
id: notificationRequest.id)
|
||||
|
||||
let requests = try await context.apiService.notificationRequests(authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
|
||||
NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil)
|
||||
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
if requests.count > 0 {
|
||||
self.viewModel.requests = requests
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationRequestsSection, NotificationRequestItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.requests.compactMap { NotificationRequestItem.item($0) } )
|
||||
|
||||
self.dataSource?.apply(snapshot)
|
||||
} else {
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rejectNotificationRequest(_ cell: NotificationRequestTableViewCell, notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) {
|
||||
|
||||
cell.rejectNotificationRequestActivityIndicatorView.isHidden = false
|
||||
cell.rejectNotificationRequestActivityIndicatorView.startAnimating()
|
||||
cell.rejectNotificationRequestButton.tintColor = .clear
|
||||
cell.rejectNotificationRequestButton.setTitleColor(.clear, for: .normal)
|
||||
cell.rejectNotificationRequestButton.isUserInteractionEnabled = false
|
||||
cell.acceptNotificationRequestButton.isUserInteractionEnabled = false
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
try await rejectNotificationRequest(notificationRequest)
|
||||
} catch {
|
||||
cell.rejectNotificationRequestActivityIndicatorView.stopAnimating()
|
||||
cell.rejectNotificationRequestButton.tintColor = .black
|
||||
cell.rejectNotificationRequestButton.setTitleColor(.black, for: .normal)
|
||||
cell.rejectNotificationRequestButton.isUserInteractionEnabled = true
|
||||
cell.acceptNotificationRequestButton.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rejectNotificationRequest(_ notificationRequest: MastodonSDK.Mastodon.Entity.NotificationRequest) async throws {
|
||||
_ = try await context.apiService.rejectNotificationRequests(authenticationBox: authContext.mastodonAuthenticationBox,
|
||||
id: notificationRequest.id)
|
||||
|
||||
let requests = try await context.apiService.notificationRequests(authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
|
||||
NotificationCenter.default.post(name: .notificationFilteringChanged, object: nil)
|
||||
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
if requests.count > 0 {
|
||||
self.viewModel.requests = requests
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationRequestsSection, NotificationRequestItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems(self.viewModel.requests.compactMap { NotificationRequestItem.item($0) } )
|
||||
|
||||
self.dataSource?.apply(snapshot)
|
||||
} else {
|
||||
_ = self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationRequestsTableViewController: AccountNotificationTimelineViewControllerDelegate {
|
||||
func acceptRequest(_ viewController: AccountNotificationTimelineViewController, request: MastodonSDK.Mastodon.Entity.NotificationRequest) {
|
||||
Task {
|
||||
try? await acceptNotificationRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissRequest(_ viewController: AccountNotificationTimelineViewController, request: MastodonSDK.Mastodon.Entity.NotificationRequest) {
|
||||
Task {
|
||||
try? await rejectNotificationRequest(request)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
struct NotificationRequestsViewModel {
|
||||
let appContext: AppContext
|
||||
let authContext: AuthContext
|
||||
let coordinator: SceneCoordinator
|
||||
|
||||
var requests: [Mastodon.Entity.NotificationRequest]
|
||||
|
||||
init(appContext: AppContext, authContext: AuthContext, coordinator: SceneCoordinator, requests: [Mastodon.Entity.NotificationRequest]) {
|
||||
self.appContext = appContext
|
||||
self.authContext = authContext
|
||||
self.coordinator = coordinator
|
||||
self.requests = requests
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import Foundation
|
||||
import MastodonSDK
|
||||
|
||||
enum NotificationItem: Hashable {
|
||||
case filteredNotifications(policy: Mastodon.Entity.NotificationPolicy)
|
||||
case feed(record: MastodonFeed)
|
||||
case feedLoader(record: MastodonFeed)
|
||||
case bottomLoader
|
||||
|
@ -39,6 +39,7 @@ extension NotificationSection {
|
||||
tableView.register(NotificationTableViewCell.self, forCellReuseIdentifier: String(describing: NotificationTableViewCell.self))
|
||||
tableView.register(AccountWarningNotificationCell.self, forCellReuseIdentifier: AccountWarningNotificationCell.reuseIdentifier)
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.register(NotificationFilteringBannerTableViewCell.self, forCellReuseIdentifier: NotificationFilteringBannerTableViewCell.reuseIdentifier)
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
@ -67,6 +68,12 @@ extension NotificationSection {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
return cell
|
||||
|
||||
case .filteredNotifications(let policy):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: NotificationFilteringBannerTableViewCell.reuseIdentifier, for: indexPath) as! NotificationFilteringBannerTableViewCell
|
||||
cell.configure(with: policy)
|
||||
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,9 @@ extension NotificationTimelineViewController: DataSourceProvider {
|
||||
}
|
||||
}()
|
||||
return item
|
||||
default:
|
||||
case .filteredNotifications(let policy):
|
||||
return DataSourceItem.notificationBanner(policy: policy)
|
||||
case .bottomLoader, .feedLoader(_):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -11,18 +11,18 @@ import CoreDataStack
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
final class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
class NotificationTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var context: AppContext!
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
let mediaPreviewTransitionController = MediaPreviewTransitionController()
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
var viewModel: NotificationTimelineViewModel!
|
||||
|
||||
let viewModel: NotificationTimelineViewModel
|
||||
|
||||
private(set) lazy var refreshControl: RefreshControl = {
|
||||
let refreshControl = RefreshControl()
|
||||
refreshControl.addTarget(self, action: #selector(NotificationTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||
@ -31,13 +31,26 @@ final class NotificationTimelineViewController: UIViewController, NeedsDependenc
|
||||
|
||||
private(set) lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.backgroundColor = .secondarySystemBackground
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
init(viewModel: NotificationTimelineViewModel, context: AppContext, coordinator: SceneCoordinator) {
|
||||
self.viewModel = viewModel
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
title = viewModel.scope.title
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewController {
|
||||
@ -113,6 +126,9 @@ extension NotificationTimelineViewController {
|
||||
|
||||
@objc private func refreshControlValueChanged(_ sender: RefreshControl) {
|
||||
Task {
|
||||
let policy = try? await context.apiService.notificationPolicy(authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
viewModel.notificationPolicy = policy?.value
|
||||
|
||||
await viewModel.loadLatest()
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ extension NotificationTimelineViewModel {
|
||||
dataController.$records
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] records in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
Task {
|
||||
@ -44,6 +44,9 @@ extension NotificationTimelineViewModel {
|
||||
}
|
||||
var snapshot = NSDiffableDataSourceSnapshot<NotificationSection, NotificationItem>()
|
||||
snapshot.appendSections([.main])
|
||||
if self.scope == .everything, let notificationPolicy = self.notificationPolicy, notificationPolicy.summary.pendingRequestsCount > 0 {
|
||||
snapshot.appendItems([.filteredNotifications(policy: notificationPolicy)])
|
||||
}
|
||||
snapshot.appendItems(newItems.removingDuplicates(), toSection: .main)
|
||||
return snapshot
|
||||
}()
|
||||
|
@ -9,6 +9,7 @@ import CoreDataStack
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
|
||||
extension NotificationTimelineViewModel {
|
||||
class LoadOldestState: GKState {
|
||||
@ -51,8 +52,22 @@ extension NotificationTimelineViewModel.LoadOldestState {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
let scope = viewModel.scope
|
||||
|
||||
let scope: APIService.MastodonNotificationScope?
|
||||
let accountID: String?
|
||||
|
||||
switch viewModel.scope {
|
||||
case .everything:
|
||||
scope = .everything
|
||||
accountID = nil
|
||||
case .mentions:
|
||||
scope = .mentions
|
||||
accountID = nil
|
||||
case .fromAccount(let account):
|
||||
scope = nil
|
||||
accountID = account.id
|
||||
}
|
||||
|
||||
Task {
|
||||
let _maxID: Mastodon.Entity.Notification.ID? = lastFeedRecord.notification?.id
|
||||
|
||||
@ -64,6 +79,7 @@ extension NotificationTimelineViewModel.LoadOldestState {
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.notifications(
|
||||
maxID: maxID,
|
||||
accountID: accountID,
|
||||
scope: scope,
|
||||
authenticationBox: viewModel.authContext.mastodonAuthenticationBox
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
|
||||
final class NotificationTimelineViewModel {
|
||||
|
||||
@ -20,6 +21,7 @@ final class NotificationTimelineViewModel {
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
let scope: Scope
|
||||
var notificationPolicy: Mastodon.Entity.NotificationPolicy?
|
||||
let dataController: FeedDataController
|
||||
@Published var isLoadingLatest = false
|
||||
@Published var lastAutomaticFetchTimestamp: Date?
|
||||
@ -46,12 +48,14 @@ final class NotificationTimelineViewModel {
|
||||
init(
|
||||
context: AppContext,
|
||||
authContext: AuthContext,
|
||||
scope: Scope
|
||||
scope: Scope,
|
||||
notificationPolicy: Mastodon.Entity.NotificationPolicy? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
self.scope = scope
|
||||
self.dataController = FeedDataController(context: context, authContext: authContext)
|
||||
self.notificationPolicy = notificationPolicy
|
||||
|
||||
switch scope {
|
||||
case .everything:
|
||||
@ -62,6 +66,8 @@ final class NotificationTimelineViewModel {
|
||||
self.dataController.records = (try? FileManager.default.cachedNotificationsMentions(for: authContext.mastodonAuthenticationBox))?.map({ notification in
|
||||
MastodonFeed.fromNotification(notification, relationship: nil, kind: .notificationMentions)
|
||||
}) ?? []
|
||||
case .fromAccount(_):
|
||||
self.dataController.records = []
|
||||
}
|
||||
|
||||
self.dataController.$records
|
||||
@ -77,18 +83,47 @@ final class NotificationTimelineViewModel {
|
||||
FileManager.default.cacheNotificationsAll(items: items, for: authContext.mastodonAuthenticationBox)
|
||||
case .mentions:
|
||||
FileManager.default.cacheNotificationsMentions(items: items, for: authContext.mastodonAuthenticationBox)
|
||||
case .fromAccount(_):
|
||||
//NOTE: we don't persist these
|
||||
break
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(Self.notificationFilteringChanged(_:)), name: .notificationFilteringChanged, object: nil)
|
||||
}
|
||||
|
||||
//MARK: - Notifications
|
||||
|
||||
@objc func notificationFilteringChanged(_ notification: Notification) {
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
let policy = try await self.context.apiService.notificationPolicy(authenticationBox: self.authContext.mastodonAuthenticationBox)
|
||||
self.notificationPolicy = policy.value
|
||||
|
||||
await self.loadLatest()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewModel {
|
||||
enum Scope: Hashable {
|
||||
case everything
|
||||
case mentions
|
||||
case fromAccount(Mastodon.Entity.Account)
|
||||
|
||||
typealias Scope = APIService.MastodonNotificationScope
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .everything:
|
||||
return L10n.Scene.Notification.Title.everything
|
||||
case .mentions:
|
||||
return L10n.Scene.Notification.Title.mentions
|
||||
case .fromAccount(let account):
|
||||
return "Notifications from \(account.displayName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewModel {
|
||||
@ -103,6 +138,8 @@ extension NotificationTimelineViewModel {
|
||||
dataController.loadInitial(kind: .notificationAll)
|
||||
case .mentions:
|
||||
dataController.loadInitial(kind: .notificationMentions)
|
||||
case .fromAccount(let account):
|
||||
dataController.loadInitial(kind: .notificationAccount(account.id))
|
||||
}
|
||||
|
||||
didLoadLatest.send()
|
||||
@ -115,6 +152,8 @@ extension NotificationTimelineViewModel {
|
||||
dataController.loadNext(kind: .notificationAll)
|
||||
case .mentions:
|
||||
dataController.loadNext(kind: .notificationMentions)
|
||||
case .fromAccount(let account):
|
||||
dataController.loadNext(kind: .notificationAccount(account.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +39,9 @@ extension NotificationView {
|
||||
|
||||
switch notification.entity.type {
|
||||
case .follow:
|
||||
setAuthorContainerBottomPaddingViewDisplay()
|
||||
setAuthorContainerBottomPaddingViewDisplay(isHidden: true)
|
||||
case .followRequest:
|
||||
setFollowRequestAdaptiveMarginContainerViewDisplay()
|
||||
setFollowRequestAdaptiveMarginContainerViewDisplay(isHidden: true)
|
||||
case .mention, .status:
|
||||
if let status = notification.status {
|
||||
statusView.configure(status: status)
|
||||
|
@ -210,7 +210,7 @@ extension NotificationView {
|
||||
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
|
||||
bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor, constant: 8),
|
||||
])
|
||||
|
||||
// author container: H - [ avatarButton | author meta container ]
|
||||
@ -327,7 +327,7 @@ extension NotificationView {
|
||||
rejectFollowRequestActivityIndicatorView.centerYAnchor.constraint(equalTo: rejectFollowRequestButton.centerYAnchor),
|
||||
])
|
||||
rejectFollowRequestActivityIndicatorView.color = .black
|
||||
acceptFollowRequestActivityIndicatorView.hidesWhenStopped = true
|
||||
rejectFollowRequestActivityIndicatorView.hidesWhenStopped = true
|
||||
rejectFollowRequestActivityIndicatorView.stopAnimating()
|
||||
|
||||
// statusView
|
||||
@ -420,12 +420,12 @@ extension NotificationView {
|
||||
|
||||
extension NotificationView {
|
||||
|
||||
public func setAuthorContainerBottomPaddingViewDisplay() {
|
||||
authorContainerViewBottomPaddingView.isHidden = false
|
||||
public func setAuthorContainerBottomPaddingViewDisplay(isHidden: Bool = false) {
|
||||
authorContainerViewBottomPaddingView.isHidden = isHidden
|
||||
}
|
||||
|
||||
public func setFollowRequestAdaptiveMarginContainerViewDisplay() {
|
||||
followRequestAdaptiveMarginContainerView.isHidden = false
|
||||
public func setFollowRequestAdaptiveMarginContainerViewDisplay(isHidden: Bool = false) {
|
||||
followRequestAdaptiveMarginContainerView.isHidden = isHidden
|
||||
}
|
||||
|
||||
public func setStatusViewDisplay() {
|
||||
|
@ -12,6 +12,7 @@ import MastodonLocalization
|
||||
import Tabman
|
||||
import Pageboy
|
||||
import MastodonCore
|
||||
import MastodonSDK
|
||||
|
||||
final class NotificationViewController: TabmanViewController, NeedsDependency {
|
||||
|
||||
@ -49,7 +50,7 @@ extension NotificationViewController {
|
||||
|
||||
view.backgroundColor = .secondarySystemBackground
|
||||
|
||||
setupSegmentedControl(scopes: APIService.MastodonNotificationScope.allCases)
|
||||
setupSegmentedControl(scopes: [.everything, .mentions])
|
||||
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationItem.titleView = pageSegmentedControl
|
||||
NSLayoutConstraint.activate([
|
||||
@ -68,7 +69,7 @@ extension NotificationViewController {
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel?.viewControllers = APIService.MastodonNotificationScope.allCases.map { scope in
|
||||
viewModel?.viewControllers = [NotificationTimelineViewModel.Scope.everything, .mentions].map { scope in
|
||||
createViewController(for: scope)
|
||||
}
|
||||
|
||||
@ -86,14 +87,11 @@ extension NotificationViewController {
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// aspectViewWillAppear(animated)
|
||||
|
||||
// fetch latest notification when scroll position is within half screen height to prevent list reload
|
||||
// if tableView.contentOffset.y < view.frame.height * 0.5 {
|
||||
// viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self)
|
||||
// }
|
||||
// https://github.com/mastodon/documentation/pull/1447#issuecomment-2149225659
|
||||
if let viewModel, viewModel.notificationPolicy != nil {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), style: .plain, target: self, action: #selector(NotificationViewController.showNotificationPolicySettings(_:)))
|
||||
}
|
||||
|
||||
|
||||
// needs trigger manually after onboarding dismiss
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
@ -117,6 +115,24 @@ extension NotificationViewController {
|
||||
|
||||
// aspectViewDidDisappear(animated)
|
||||
}
|
||||
|
||||
//MARK: - Actions
|
||||
|
||||
@objc private func showNotificationPolicySettings(_ sender: Any) {
|
||||
guard let viewModel, let policy = viewModel.notificationPolicy else { return }
|
||||
|
||||
let policyViewModel = NotificationFilterViewModel(
|
||||
appContext: viewModel.context,
|
||||
notFollowing: policy.filterNotFollowing,
|
||||
noFollower: policy.filterNotFollowers,
|
||||
newAccount: policy.filterNewAccounts,
|
||||
privateMentions: policy.filterPrivateMentions
|
||||
)
|
||||
|
||||
guard let policyViewController = coordinator.present(scene: .notificationPolicy(viewModel: policyViewModel), transition: .formSheet) as? NotificationPolicyViewController else { return }
|
||||
|
||||
policyViewController.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationViewController {
|
||||
@ -134,17 +150,20 @@ extension NotificationViewController {
|
||||
pageSegmentedControl.selectedSegmentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createViewController(for scope: NotificationTimelineViewModel.Scope) -> UIViewController {
|
||||
guard let authContext = viewModel?.authContext else { return UITableViewController() }
|
||||
let viewController = NotificationTimelineViewController()
|
||||
viewController.context = context
|
||||
viewController.coordinator = coordinator
|
||||
viewController.viewModel = NotificationTimelineViewModel(
|
||||
guard let viewModel else { return UITableViewController() }
|
||||
|
||||
let viewController = NotificationTimelineViewController(
|
||||
viewModel: NotificationTimelineViewModel(
|
||||
context: context,
|
||||
authContext: viewModel.authContext,
|
||||
scope: scope, notificationPolicy: viewModel.notificationPolicy
|
||||
),
|
||||
context: context,
|
||||
authContext: authContext,
|
||||
scope: scope
|
||||
coordinator: coordinator
|
||||
)
|
||||
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
@ -234,3 +253,12 @@ extension NotificationViewController {
|
||||
return categorySwitchKeyCommands
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//MARK: - NotificationPolicyViewControllerDelegate
|
||||
|
||||
extension NotificationViewController: NotificationPolicyViewControllerDelegate {
|
||||
func policyUpdated(_ viewController: NotificationPolicyViewController, newPolicy: Mastodon.Entity.NotificationPolicy) {
|
||||
viewModel?.notificationPolicy = newPolicy
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import Pageboy
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import MastodonSDK
|
||||
|
||||
final class NotificationViewModel {
|
||||
|
||||
@ -19,6 +20,7 @@ final class NotificationViewModel {
|
||||
// input
|
||||
let context: AppContext
|
||||
let authContext: AuthContext
|
||||
var notificationPolicy: Mastodon.Entity.NotificationPolicy?
|
||||
let viewDidLoad = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
@ -50,17 +52,15 @@ final class NotificationViewModel {
|
||||
init(context: AppContext, authContext: AuthContext) {
|
||||
self.context = context
|
||||
self.authContext = authContext
|
||||
|
||||
// end init
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationTimelineViewModel.Scope {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .everything:
|
||||
return L10n.Scene.Notification.Title.everything
|
||||
case .mentions:
|
||||
return L10n.Scene.Notification.Title.mentions
|
||||
Task {
|
||||
do {
|
||||
let policy = try await context.apiService.notificationPolicy(authenticationBox: authContext.mastodonAuthenticationBox)
|
||||
self.notificationPolicy = policy.value
|
||||
} catch {
|
||||
// we won't show the filtering-options.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ extension SearchResultViewController {
|
||||
provider: self,
|
||||
tag: tag
|
||||
)
|
||||
case .notification:
|
||||
case .notification, .notificationBanner(_):
|
||||
assertionFailure()
|
||||
} // end switch
|
||||
|
||||
|
@ -17,6 +17,7 @@ class GeneralSettingToggleTableViewCell: ToggleTableViewCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
subtitleLabel.isHidden = true
|
||||
toggle.addTarget(self, action: #selector(GeneralSettingToggleTableViewCell.toggleValueChanged(_:)), for: .valueChanged)
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ class NotificationSettingTableViewToggleCell: ToggleTableViewCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
subtitleLabel.isHidden = true
|
||||
|
||||
toggle.addTarget(self, action: #selector(NotificationSettingTableViewToggleCell.toggleValueChanged(_:)), for: .valueChanged)
|
||||
}
|
||||
|
@ -9,22 +9,33 @@ class ToggleTableViewCell: UITableViewCell {
|
||||
}
|
||||
|
||||
let label: UILabel
|
||||
let subtitleLabel: UILabel
|
||||
private let labelStackView: UIStackView
|
||||
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
|
||||
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
subtitleLabel.numberOfLines = 0
|
||||
|
||||
labelStackView = UIStackView(arrangedSubviews: [label, subtitleLabel])
|
||||
labelStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
labelStackView.alignment = .leading
|
||||
labelStackView.axis = .vertical
|
||||
labelStackView.spacing = 4
|
||||
|
||||
toggle = UISwitch()
|
||||
toggle.translatesAutoresizingMaskIntoConstraints = false
|
||||
toggle.onTintColor = Asset.Colors.Brand.blurple.color
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
contentView.addSubview(label)
|
||||
contentView.addSubview(labelStackView)
|
||||
contentView.addSubview(toggle)
|
||||
setupConstraints()
|
||||
}
|
||||
@ -33,11 +44,11 @@ class ToggleTableViewCell: UITableViewCell {
|
||||
|
||||
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),
|
||||
labelStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
|
||||
labelStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
contentView.bottomAnchor.constraint(equalTo: labelStackView.bottomAnchor, constant: 11),
|
||||
|
||||
toggle.leadingAnchor.constraint(greaterThanOrEqualTo: labelStackView.trailingAnchor, constant: 16),
|
||||
toggle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: toggle.trailingAnchor, constant: 16)
|
||||
|
||||
|
@ -202,13 +202,14 @@ private extension FeedDataController {
|
||||
return try await getFeeds(with: .everything)
|
||||
case .notificationMentions:
|
||||
return try await getFeeds(with: .mentions)
|
||||
|
||||
case .notificationAccount(let accountID):
|
||||
return try await getFeeds(with: nil, accountID: accountID)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFeeds(with scope: APIService.MastodonNotificationScope) async throws -> [MastodonFeed] {
|
||||
private func getFeeds(with scope: APIService.MastodonNotificationScope?, accountID: String? = nil) async throws -> [MastodonFeed] {
|
||||
|
||||
let notifications = try await context.apiService.notifications(maxID: nil, scope: scope, authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
let notifications = try await context.apiService.notifications(maxID: nil, accountID: accountID, scope: scope, authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
|
||||
let accounts = notifications.map { $0.account }
|
||||
let relationships = try await context.apiService.relationship(forAccounts: accounts, authenticationBox: authContext.mastodonAuthenticationBox).value
|
||||
|
@ -10,70 +10,42 @@ import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import OSLog
|
||||
|
||||
extension APIService {
|
||||
|
||||
public enum MastodonNotificationScope: String, Hashable, CaseIterable {
|
||||
case everything
|
||||
case mentions
|
||||
|
||||
public var includeTypes: [MastodonNotificationType]? {
|
||||
switch self {
|
||||
case .everything: return nil
|
||||
case .mentions: return [.mention, .status]
|
||||
}
|
||||
}
|
||||
|
||||
public var excludeTypes: [MastodonNotificationType]? {
|
||||
switch self {
|
||||
case .everything: return nil
|
||||
case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll]
|
||||
}
|
||||
}
|
||||
|
||||
public var _excludeTypes: [Mastodon.Entity.Notification.NotificationType]? {
|
||||
switch self {
|
||||
case .everything: return nil
|
||||
case .mentions: return [.follow, .followRequest, .reblog, .favourite, .poll]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func notifications(
|
||||
maxID: Mastodon.Entity.Status.ID?,
|
||||
scope: MastodonNotificationScope,
|
||||
accountID: String? = nil,
|
||||
scope: MastodonNotificationScope?,
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.Notification]> {
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
|
||||
let types: [Mastodon.Entity.Notification.NotificationType]?
|
||||
let excludedTypes: [Mastodon.Entity.Notification.NotificationType]?
|
||||
|
||||
switch scope {
|
||||
case .everything:
|
||||
types = [.follow, .followRequest, .mention, .reblog, .favourite, .poll, .status, .moderationWarning]
|
||||
excludedTypes = nil
|
||||
case .mentions:
|
||||
types = [.mention]
|
||||
excludedTypes = [.follow, .followRequest, .reblog, .favourite, .poll]
|
||||
case nil:
|
||||
types = nil
|
||||
excludedTypes = nil
|
||||
}
|
||||
|
||||
let query = Mastodon.API.Notifications.Query(
|
||||
maxID: maxID,
|
||||
types: {
|
||||
switch scope {
|
||||
case .everything:
|
||||
return [
|
||||
.follow,
|
||||
.followRequest,
|
||||
.mention,
|
||||
.reblog,
|
||||
.favourite,
|
||||
.poll,
|
||||
.status,
|
||||
.moderationWarning
|
||||
]
|
||||
case .mentions:
|
||||
return [.mention]
|
||||
}
|
||||
}(),
|
||||
excludeTypes: {
|
||||
switch scope {
|
||||
case .everything:
|
||||
return nil
|
||||
case .mentions:
|
||||
return [.follow, .followRequest, .reblog, .favourite, .poll]
|
||||
}
|
||||
}()
|
||||
types: types,
|
||||
excludeTypes: excludedTypes,
|
||||
accountID: accountID
|
||||
)
|
||||
|
||||
let response = try await Mastodon.API.Notifications.getNotifications(
|
||||
@ -107,3 +79,70 @@ extension APIService {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: - Notification Policy
|
||||
|
||||
extension APIService {
|
||||
public func notificationPolicy(authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy> {
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let response = try await Mastodon.API.Notifications.getNotificationPolicy(session: session, domain: domain, authorization: authorization)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
public func updateNotificationPolicy(
|
||||
authenticationBox: MastodonAuthenticationBox,
|
||||
filterNotFollowing: Bool,
|
||||
filterNotFollowers: Bool,
|
||||
filterNewAccounts: Bool,
|
||||
filterPrivateMentions: Bool
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy> {
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
let query = Mastodon.API.Notifications.UpdateNotificationPolicyQuery(filterNotFollowing: filterNotFollowing, filterNotFollowers: filterNotFollowers, filterNewAccounts: filterNewAccounts, filterPrivateMentions: filterPrivateMentions)
|
||||
|
||||
let response = try await Mastodon.API.Notifications.updateNotificationPolicy(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization,
|
||||
query: query
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notification Requests
|
||||
|
||||
extension APIService {
|
||||
public func notificationRequests(authenticationBox: MastodonAuthenticationBox) async throws -> Mastodon.Response.Content<[Mastodon.Entity.NotificationRequest]> {
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let response = try await Mastodon.API.Notifications.getNotificationRequests(session: session, domain: domain, authorization: authorization)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
public func acceptNotificationRequests(authenticationBox: MastodonAuthenticationBox, id: String) async throws -> Mastodon.Response.Content<[String: String]> {
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let response = try await Mastodon.API.Notifications.acceptNotificationRequest(id: id, session: session, domain: domain, authorization: authorization)
|
||||
return response
|
||||
}
|
||||
|
||||
public func rejectNotificationRequests(authenticationBox: MastodonAuthenticationBox, id: String) async throws -> Mastodon.Response.Content<[String: String]> {
|
||||
let domain = authenticationBox.domain
|
||||
let authorization = authenticationBox.userAuthorization
|
||||
|
||||
let response = try await Mastodon.API.Notifications.dismissNotificationRequest(id: id, session: session, domain: domain, authorization: authorization)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
public static let notificationFilteringChanged = Notification.Name(rawValue: "org.joinmastodon.app.notificationFilteringsChanged")
|
||||
}
|
||||
|
@ -899,6 +899,14 @@ public enum L10n {
|
||||
}
|
||||
}
|
||||
public enum Notification {
|
||||
public enum FilteredNotification {
|
||||
/// Accept
|
||||
public static let accept = L10n.tr("Localizable", "Scene.Notification.FilteredNotification.Accept", fallback: "Accept")
|
||||
/// Dismiss
|
||||
public static let dismiss = L10n.tr("Localizable", "Scene.Notification.FilteredNotification.Dismiss", fallback: "Dismiss")
|
||||
/// Filtered Notifications
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.FilteredNotification.Title", fallback: "Filtered Notifications")
|
||||
}
|
||||
public enum FollowRequest {
|
||||
/// Accept
|
||||
public static let accept = L10n.tr("Localizable", "Scene.Notification.FollowRequest.Accept", fallback: "Accept")
|
||||
@ -929,6 +937,34 @@ public enum L10n {
|
||||
/// request to follow you
|
||||
public static let requestToFollowYou = L10n.tr("Localizable", "Scene.Notification.NotificationDescription.RequestToFollowYou", fallback: "request to follow you")
|
||||
}
|
||||
public enum Policy {
|
||||
/// Filter Notifications from…
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.Title", fallback: "Filter Notifications from…")
|
||||
public enum NewAccount {
|
||||
/// Created within the past 30 days
|
||||
public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.NewAccount.Subtitle", fallback: "Created within the past 30 days")
|
||||
/// New accounts
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.NewAccount.Title", fallback: "New accounts")
|
||||
}
|
||||
public enum NoFollower {
|
||||
/// Including people who have been following you fewer than 3 days
|
||||
public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.NoFollower.Subtitle", fallback: "Including people who have been following you fewer than 3 days")
|
||||
/// People not following you
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.NoFollower.Title", fallback: "People not following you")
|
||||
}
|
||||
public enum NotFollowing {
|
||||
/// Until you manually approve them
|
||||
public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.NotFollowing.Subtitle", fallback: "Until you manually approve them")
|
||||
/// People you don't follow
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.NotFollowing.Title", fallback: "People you don't follow")
|
||||
}
|
||||
public enum PrivateMentions {
|
||||
/// Filtered unless it’s in reply to your own mention or if you follow the sender
|
||||
public static let subtitle = L10n.tr("Localizable", "Scene.Notification.Policy.PrivateMentions.Subtitle", fallback: "Filtered unless it’s in reply to your own mention or if you follow the sender")
|
||||
/// Unsolicited private mentions
|
||||
public static let title = L10n.tr("Localizable", "Scene.Notification.Policy.PrivateMentions.Title", fallback: "Unsolicited private mentions")
|
||||
}
|
||||
}
|
||||
public enum Title {
|
||||
/// Everything
|
||||
public static let everything = L10n.tr("Localizable", "Scene.Notification.Title.Everything", fallback: "Everything")
|
||||
@ -1954,6 +1990,12 @@ public enum L10n {
|
||||
}
|
||||
}
|
||||
}
|
||||
public enum FilteredNotificationBanner {
|
||||
/// Plural format key: "%#@number_of_requests@"
|
||||
public static func subtitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "plural.filtered_notification_banner.subtitle", p1, fallback: "Plural format key: \"%#@number_of_requests@\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
|
@ -305,11 +305,11 @@ uploaded to Mastodon.";
|
||||
"Scene.Following.Title" = "following";
|
||||
"Scene.HomeTimeline.EmptyState.ListEmptyMessageTitle" = "This list is empty";
|
||||
"Scene.HomeTimeline.TimelineMenu.Following" = "Following";
|
||||
"Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local";
|
||||
"Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists";
|
||||
"Scene.HomeTimeline.TimelineMenu.Lists.EmptyMessage" = "You don't have any Lists";
|
||||
"Scene.HomeTimeline.TimelineMenu.Hashtags.Title" = "Followed Hashtags";
|
||||
"Scene.HomeTimeline.TimelineMenu.Hashtags.EmptyMessage" = "You don't follow any Hashtags";
|
||||
"Scene.HomeTimeline.TimelineMenu.Hashtags.Title" = "Followed Hashtags";
|
||||
"Scene.HomeTimeline.TimelineMenu.Lists.EmptyMessage" = "You don't have any Lists";
|
||||
"Scene.HomeTimeline.TimelineMenu.Lists.Title" = "Lists";
|
||||
"Scene.HomeTimeline.TimelineMenu.LocalCommunity" = "Local";
|
||||
"Scene.HomeTimeline.TimelinePill.NewPosts" = "New Posts";
|
||||
"Scene.HomeTimeline.TimelinePill.Offline" = "Offline";
|
||||
"Scene.HomeTimeline.TimelinePill.PostSent" = "Post Sent";
|
||||
@ -317,6 +317,9 @@ uploaded to Mastodon.";
|
||||
"Scene.Login.ServerSearchField.Placeholder" = "Enter URL or search for your server";
|
||||
"Scene.Login.Subtitle" = "Log in with the server where you created your account.";
|
||||
"Scene.Login.Title" = "Welcome Back";
|
||||
"Scene.Notification.FilteredNotification.Accept" = "Accept";
|
||||
"Scene.Notification.FilteredNotification.Dismiss" = "Dismiss";
|
||||
"Scene.Notification.FilteredNotification.Title" = "Filtered Notifications";
|
||||
"Scene.Notification.FollowRequest.Accept" = "Accept";
|
||||
"Scene.Notification.FollowRequest.Accepted" = "Accepted";
|
||||
"Scene.Notification.FollowRequest.Reject" = "reject";
|
||||
@ -329,6 +332,15 @@ uploaded to Mastodon.";
|
||||
"Scene.Notification.NotificationDescription.PollHasEnded" = "poll has ended";
|
||||
"Scene.Notification.NotificationDescription.RebloggedYourPost" = "boosted your post";
|
||||
"Scene.Notification.NotificationDescription.RequestToFollowYou" = "request to follow you";
|
||||
"Scene.Notification.Policy.NewAccount.Subtitle" = "Created within the past 30 days";
|
||||
"Scene.Notification.Policy.NewAccount.Title" = "New accounts";
|
||||
"Scene.Notification.Policy.NoFollower.Subtitle" = "Including people who have been following you fewer than 3 days";
|
||||
"Scene.Notification.Policy.NoFollower.Title" = "People not following you";
|
||||
"Scene.Notification.Policy.NotFollowing.Subtitle" = "Until you manually approve them";
|
||||
"Scene.Notification.Policy.NotFollowing.Title" = "People you don't follow";
|
||||
"Scene.Notification.Policy.PrivateMentions.Subtitle" = "Filtered unless it’s in reply to your own mention or if you follow the sender";
|
||||
"Scene.Notification.Policy.PrivateMentions.Title" = "Unsolicited private mentions";
|
||||
"Scene.Notification.Policy.Title" = "Filter Notifications from…";
|
||||
"Scene.Notification.Title.Everything" = "Everything";
|
||||
"Scene.Notification.Title.Mentions" = "Mentions";
|
||||
"Scene.Notification.Warning.DeleteStatuses" = "Some of your posts have been removed.";
|
||||
@ -622,4 +634,4 @@ If you disagree with the policy for **%@**, you can go back and pick a different
|
||||
"Widget.MultipleFollowers.ConfigurationDescription" = "Show number of followers for multiple accounts.";
|
||||
"Widget.MultipleFollowers.ConfigurationDisplayName" = "Multiple followers";
|
||||
"Widget.MultipleFollowers.MockUser.AccountName" = "another@follower.social";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
||||
"Widget.MultipleFollowers.MockUser.DisplayName" = "Another follower";
|
File diff suppressed because it is too large
Load Diff
@ -1,481 +1,497 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>a11y.plural.count.unread.notification</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@notification_count_unread_notification@</string>
|
||||
<key>notification_count_unread_notification</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 unread notification</string>
|
||||
<key>other</key>
|
||||
<string>%ld unread notifications</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.input_limit_exceeds</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>Input limit exceeds %#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.input_limit_remains</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>Input limit remains %#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.characters_left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character left</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.followed_by_and_mutual</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@names@%#@count_mutual@</string>
|
||||
<key>names</key>
|
||||
<dict>
|
||||
<key>one</key>
|
||||
<string></string>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>other</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>count_mutual</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>Followed by %1$@, and another mutual</string>
|
||||
<key>other</key>
|
||||
<string>Followed by %1$@, and %ld mutuals</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.metric_formatted.post</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%@ %#@post_count@</string>
|
||||
<key>post_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>post</string>
|
||||
<key>other</key>
|
||||
<string>posts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.media</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@media_count@</string>
|
||||
<key>media_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 media</string>
|
||||
<key>other</key>
|
||||
<string>%ld media</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.post</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@post_count@</string>
|
||||
<key>post_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 post</string>
|
||||
<key>other</key>
|
||||
<string>%ld posts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.favorite</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@favorite_count@</string>
|
||||
<key>favorite_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 favorite</string>
|
||||
<key>other</key>
|
||||
<string>%ld favorites</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reblog</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reblog_count@</string>
|
||||
<key>reblog_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 reblog</string>
|
||||
<key>other</key>
|
||||
<string>%ld reblogs</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reblog_a11y</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reblog_count@</string>
|
||||
<key>reblog_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 re-blog</string>
|
||||
<key>other</key>
|
||||
<string>%ld re-blogs</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reply</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reply_count@</string>
|
||||
<key>reply_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 reply</string>
|
||||
<key>other</key>
|
||||
<string>%ld replies</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.vote</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@vote_count@</string>
|
||||
<key>vote_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 vote</string>
|
||||
<key>other</key>
|
||||
<string>%ld votes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.voter</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@voter_count@</string>
|
||||
<key>voter_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 voter</string>
|
||||
<key>other</key>
|
||||
<string>%ld voters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.people_talking</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_people_talking@</string>
|
||||
<key>count_people_talking</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 people talking</string>
|
||||
<key>other</key>
|
||||
<string>%ld people talking</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.following</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_following@</string>
|
||||
<key>count_following</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 following</string>
|
||||
<key>other</key>
|
||||
<string>%ld following</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.follower</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_follower@</string>
|
||||
<key>count_follower</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 follower</string>
|
||||
<key>other</key>
|
||||
<string>%ld followers</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.year.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_year_left@</string>
|
||||
<key>count_year_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 year left</string>
|
||||
<key>other</key>
|
||||
<string>%ld years left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.month.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_month_left@</string>
|
||||
<key>count_month_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 months left</string>
|
||||
<key>other</key>
|
||||
<string>%ld months left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.day.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_day_left@</string>
|
||||
<key>count_day_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 day left</string>
|
||||
<key>other</key>
|
||||
<string>%ld days left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.hour.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_hour_left@</string>
|
||||
<key>count_hour_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 hour left</string>
|
||||
<key>other</key>
|
||||
<string>%ld hours left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.minute.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_minute_left@</string>
|
||||
<key>count_minute_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 minute left</string>
|
||||
<key>other</key>
|
||||
<string>%ld minutes left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.second.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_second_left@</string>
|
||||
<key>count_second_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 second left</string>
|
||||
<key>other</key>
|
||||
<string>%ld seconds left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.year.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_year_ago_abbr@</string>
|
||||
<key>count_year_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1y ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldy ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.month.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_month_ago_abbr@</string>
|
||||
<key>count_month_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1M ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldM ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.day.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_day_ago_abbr@</string>
|
||||
<key>count_day_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1d ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldd ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.hour.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_hour_ago_abbr@</string>
|
||||
<key>count_hour_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1h ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldh ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.minute.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_minute_ago_abbr@</string>
|
||||
<key>count_minute_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1m ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldm ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.second.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_second_ago_abbr@</string>
|
||||
<key>count_second_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1s ago</string>
|
||||
<key>other</key>
|
||||
<string>%lds ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>a11y.plural.count.unread.notification</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@notification_count_unread_notification@</string>
|
||||
<key>notification_count_unread_notification</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 unread notification</string>
|
||||
<key>other</key>
|
||||
<string>%ld unread notifications</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.input_limit_exceeds</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>Input limit exceeds %#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.input_limit_remains</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>Input limit remains %#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>a11y.plural.count.characters_left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@character_count@</string>
|
||||
<key>character_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 character left</string>
|
||||
<key>other</key>
|
||||
<string>%ld characters left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.followed_by_and_mutual</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@names@%#@count_mutual@</string>
|
||||
<key>names</key>
|
||||
<dict>
|
||||
<key>one</key>
|
||||
<string></string>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>other</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>count_mutual</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>Followed by %1$@, and another mutual</string>
|
||||
<key>other</key>
|
||||
<string>Followed by %1$@, and %ld mutuals</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.metric_formatted.post</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%@ %#@post_count@</string>
|
||||
<key>post_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>post</string>
|
||||
<key>other</key>
|
||||
<string>posts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.media</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@media_count@</string>
|
||||
<key>media_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 media</string>
|
||||
<key>other</key>
|
||||
<string>%ld media</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.post</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@post_count@</string>
|
||||
<key>post_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 post</string>
|
||||
<key>other</key>
|
||||
<string>%ld posts</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.favorite</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@favorite_count@</string>
|
||||
<key>favorite_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 favorite</string>
|
||||
<key>other</key>
|
||||
<string>%ld favorites</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reblog</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reblog_count@</string>
|
||||
<key>reblog_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 reblog</string>
|
||||
<key>other</key>
|
||||
<string>%ld reblogs</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reblog_a11y</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reblog_count@</string>
|
||||
<key>reblog_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 re-blog</string>
|
||||
<key>other</key>
|
||||
<string>%ld re-blogs</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.reply</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@reply_count@</string>
|
||||
<key>reply_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 reply</string>
|
||||
<key>other</key>
|
||||
<string>%ld replies</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.vote</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@vote_count@</string>
|
||||
<key>vote_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 vote</string>
|
||||
<key>other</key>
|
||||
<string>%ld votes</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.voter</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@voter_count@</string>
|
||||
<key>voter_count</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 voter</string>
|
||||
<key>other</key>
|
||||
<string>%ld voters</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.people_talking</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_people_talking@</string>
|
||||
<key>count_people_talking</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 people talking</string>
|
||||
<key>other</key>
|
||||
<string>%ld people talking</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.following</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_following@</string>
|
||||
<key>count_following</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 following</string>
|
||||
<key>other</key>
|
||||
<string>%ld following</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.count.follower</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_follower@</string>
|
||||
<key>count_follower</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 follower</string>
|
||||
<key>other</key>
|
||||
<string>%ld followers</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.year.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_year_left@</string>
|
||||
<key>count_year_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 year left</string>
|
||||
<key>other</key>
|
||||
<string>%ld years left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.month.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_month_left@</string>
|
||||
<key>count_month_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 months left</string>
|
||||
<key>other</key>
|
||||
<string>%ld months left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.day.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_day_left@</string>
|
||||
<key>count_day_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 day left</string>
|
||||
<key>other</key>
|
||||
<string>%ld days left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.hour.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_hour_left@</string>
|
||||
<key>count_hour_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 hour left</string>
|
||||
<key>other</key>
|
||||
<string>%ld hours left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.minute.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_minute_left@</string>
|
||||
<key>count_minute_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 minute left</string>
|
||||
<key>other</key>
|
||||
<string>%ld minutes left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.second.left</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_second_left@</string>
|
||||
<key>count_second_left</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1 second left</string>
|
||||
<key>other</key>
|
||||
<string>%ld seconds left</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.year.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_year_ago_abbr@</string>
|
||||
<key>count_year_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1y ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldy ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.month.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_month_ago_abbr@</string>
|
||||
<key>count_month_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1M ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldM ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.day.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_day_ago_abbr@</string>
|
||||
<key>count_day_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1d ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldd ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.hour.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_hour_ago_abbr@</string>
|
||||
<key>count_hour_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1h ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldh ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.minute.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_minute_ago_abbr@</string>
|
||||
<key>count_minute_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1m ago</string>
|
||||
<key>other</key>
|
||||
<string>%ldm ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>date.second.ago.abbr</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@count_second_ago_abbr@</string>
|
||||
<key>count_second_ago_abbr</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>1s ago</string>
|
||||
<key>other</key>
|
||||
<string>%lds ago</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>plural.filtered_notification_banner.subtitle</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@number_of_requests@</string>
|
||||
<key>number_of_requests</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>ld</string>
|
||||
<key>one</key>
|
||||
<string>One person you may know</string>
|
||||
<key>other</key>
|
||||
<string>%ld people you may know</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -134,3 +134,135 @@ extension Mastodon.API.Notifications {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notification Policy
|
||||
|
||||
extension Mastodon.API.Notifications {
|
||||
internal static func notificationPolicyEndpointURL(domain: String) -> URL {
|
||||
notificationsEndpointURL(domain: domain).appendingPathComponent("policy")
|
||||
}
|
||||
|
||||
public struct UpdateNotificationPolicyQuery: Codable, PatchQuery {
|
||||
public let filterNotFollowing: Bool
|
||||
public let filterNotFollowers: Bool
|
||||
public let filterNewAccounts: Bool
|
||||
public let filterPrivateMentions: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case filterNotFollowing = "filter_not_following"
|
||||
case filterNotFollowers = "filter_not_followers"
|
||||
case filterNewAccounts = "filter_new_accounts"
|
||||
case filterPrivateMentions = "filter_private_mentions"
|
||||
}
|
||||
|
||||
public init(filterNotFollowing: Bool, filterNotFollowers: Bool, filterNewAccounts: Bool, filterPrivateMentions: Bool) {
|
||||
self.filterNotFollowing = filterNotFollowing
|
||||
self.filterNotFollowers = filterNotFollowers
|
||||
self.filterNewAccounts = filterNewAccounts
|
||||
self.filterPrivateMentions = filterPrivateMentions
|
||||
}
|
||||
}
|
||||
|
||||
public static func getNotificationPolicy(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy> {
|
||||
let request = Mastodon.API.get(
|
||||
url: notificationPolicyEndpointURL(domain: domain),
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.NotificationPolicy.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
|
||||
public static func updateNotificationPolicy(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization,
|
||||
query: Mastodon.API.Notifications.UpdateNotificationPolicyQuery
|
||||
) async throws -> Mastodon.Response.Content<Mastodon.Entity.NotificationPolicy> {
|
||||
let request = Mastodon.API.patch(
|
||||
url: notificationPolicyEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
let (data, response) = try await session.data(for: request)
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.NotificationPolicy.self, from: data, response: response)
|
||||
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.API.Notifications {
|
||||
internal static func notificationRequestsEndpointURL(domain: String) -> URL {
|
||||
notificationsEndpointURL(domain: domain).appendingPathComponent("requests")
|
||||
}
|
||||
|
||||
internal static func notificationRequestEndpointURL(domain: String, id: String) -> URL {
|
||||
notificationRequestsEndpointURL(domain: domain).appendingPathComponent(id)
|
||||
}
|
||||
|
||||
internal static func acceptNotificationRequestEndpointURL(domain: String, id: String) -> URL {
|
||||
notificationRequestEndpointURL(domain: domain, id: id).appendingPathComponent("accept")
|
||||
}
|
||||
|
||||
internal static func dismissNotificationRequestEndpointURL(domain: String, id: String) -> URL {
|
||||
notificationRequestEndpointURL(domain: domain, id: id).appendingPathComponent("dismiss")
|
||||
}
|
||||
|
||||
public static func getNotificationRequests(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Response.Content<[Mastodon.Entity.NotificationRequest]> {
|
||||
let request = Mastodon.API.get(
|
||||
url: notificationRequestsEndpointURL(domain: domain),
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.NotificationRequest].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
|
||||
public static func acceptNotificationRequest(
|
||||
id: String,
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Response.Content<[String: String]> {
|
||||
let request = Mastodon.API.post(
|
||||
url: acceptNotificationRequestEndpointURL(domain: domain, id: id),
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
// we expect an empty dictionary
|
||||
let value = try Mastodon.API.decode(type: [String: String].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
|
||||
public static func dismissNotificationRequest(
|
||||
id: String,
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) async throws -> Mastodon.Response.Content<[String: String]> {
|
||||
let request = Mastodon.API.post(
|
||||
url: dismissNotificationRequestEndpointURL(domain: domain, id: id),
|
||||
authorization: authorization
|
||||
)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
// we expect an empty dictionary
|
||||
let value = try Mastodon.API.decode(type: [String: String].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ extension Mastodon.API {
|
||||
|
||||
static func post(
|
||||
url: URL,
|
||||
query: PostQuery?,
|
||||
query: PostQuery? = nil,
|
||||
authorization: OAuth.Authorization? = nil
|
||||
) -> URLRequest {
|
||||
return buildRequest(url: url, method: .POST, query: query, authorization: authorization)
|
||||
|
@ -0,0 +1,31 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public struct NotificationPolicy: Codable, Hashable {
|
||||
public let filterNotFollowing: Bool
|
||||
public let filterNotFollowers: Bool
|
||||
public let filterNewAccounts: Bool
|
||||
public let filterPrivateMentions: Bool
|
||||
public let summary: Summary
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case filterNotFollowing = "filter_not_following"
|
||||
case filterNotFollowers = "filter_not_followers"
|
||||
case filterNewAccounts = "filter_new_accounts"
|
||||
case filterPrivateMentions = "filter_private_mentions"
|
||||
case summary
|
||||
}
|
||||
|
||||
public struct Summary: Codable, Hashable {
|
||||
public let pendingRequestsCount: Int
|
||||
public let pendingNotificationsCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pendingRequestsCount = "pending_requests_count"
|
||||
case pendingNotificationsCount = "pending_notifications_count"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public struct NotificationRequest: Codable, Hashable {
|
||||
public let id: String
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
public let account: Mastodon.Entity.Account
|
||||
public let notificationsCount: String // contains an `Int`
|
||||
public let lastStatus: Mastodon.Entity.Status?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case account = "account"
|
||||
case notificationsCount = "notifications_count"
|
||||
case lastStatus = "last_status"
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ public final class MastodonFeed {
|
||||
case home(timeline: TimelineContext)
|
||||
case notificationAll
|
||||
case notificationMentions
|
||||
case notificationAccount(String)
|
||||
|
||||
public enum TimelineContext: Equatable {
|
||||
case home
|
||||
|
@ -55,6 +55,11 @@ extension PostQuery {
|
||||
// PATCH
|
||||
protocol PatchQuery: RequestQuery { }
|
||||
|
||||
extension PatchQuery {
|
||||
// By default a `PatchQuery` does not have query items
|
||||
var queryItems: [URLQueryItem]? { nil }
|
||||
}
|
||||
|
||||
// PUT
|
||||
protocol PutQuery: RequestQuery { }
|
||||
|
||||
|
@ -290,19 +290,3 @@ extension ActionToolbarContainer {
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct ActionToolbarContainer_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UIViewPreview(width: 300) {
|
||||
ActionToolbarContainer()
|
||||
}
|
||||
.previewLayout(.fixed(width: 300, height: 44))
|
||||
.previewDisplayName("Inline")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user