diff --git a/Localization/Localizable.stringsdict b/Localization/Localizable.stringsdict index a56e4b1e64..1038e2ea85 100644 --- a/Localization/Localizable.stringsdict +++ b/Localization/Localizable.stringsdict @@ -2,72 +2,72 @@ - a11y.plural.count.unread.notification - - NSStringLocalizedFormatKey - %#@notification_count_unread_notification@ - notification_count_unread_notification - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - no unread notifications - one - 1 unread notification - few - %ld unread notifications - many - %ld unread notifications - other - %ld unread notifications - - - a11y.plural.count.input_limit_exceeds - - NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - - a11y.plural.count.input_limit_remains - - NSStringLocalizedFormatKey - Input limit remains %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no unread notifications + one + 1 unread notification + few + %ld unread notifications + many + %ld unread notifications + other + %ld unread notifications + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + a11y.plural.count.characters_left NSStringLocalizedFormatKey @@ -90,125 +90,125 @@ %ld characters left - plural.count.followed_by_and_mutual - - NSStringLocalizedFormatKey - %#@names@%#@count_mutual@ - names - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - other - - - count_mutual - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - Followed by %1$@ - one - Followed by %1$@, and another mutual - few - Followed by %1$@, and %ld mutuals - many - Followed by %1$@, and %ld mutuals - other - Followed by %1$@, and %ld mutuals - - - plural.count.metric_formatted.post - - NSStringLocalizedFormatKey - %@ %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - posts - one - post - few - posts - many - posts - other - posts - - - plural.count.media - - NSStringLocalizedFormatKey - %#@media_count@ - media_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 media - one - 1 media - few - %ld media - many - %ld media - other - %ld media - - - plural.count.post - - NSStringLocalizedFormatKey - %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 posts - one - 1 post - few - %ld posts - many - %ld posts - other - %ld posts - - + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@ + one + Followed by %1$@, and another mutual + few + Followed by %1$@, and %ld mutuals + many + Followed by %1$@, and %ld mutuals + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 media + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 posts + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + plural.count.favorite - - NSStringLocalizedFormatKey - %#@favorite_count@ - favorite_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 favorites - one - 1 favorite - few - %ld favorites - many - %ld favorites - other - %ld favorites - - + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + plural.count.reblog NSStringLocalizedFormatKey @@ -231,29 +231,29 @@ %ld reblogs - plural.count.reblog_a11y - - NSStringLocalizedFormatKey - %#@reblog_count@ - reblog_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 re-blogs - one - 1 re-blog - few - %ld re-blogs - many - %ld re-blogs - other - %ld re-blogs - - - plural.count.reply + plural.count.reblog_a11y + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 re-blogs + one + 1 re-blog + few + %ld re-blogs + many + %ld re-blogs + other + %ld re-blogs + + + plural.count.reply NSStringLocalizedFormatKey %#@reply_count@ @@ -275,379 +275,395 @@ %ld replies - plural.count.vote - - NSStringLocalizedFormatKey - %#@vote_count@ - vote_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 votes - one - 1 vote - few - %ld votes - many - %ld votes - other - %ld votes - - - plural.count.voter - - NSStringLocalizedFormatKey - %#@voter_count@ - voter_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 voters - one - 1 voter - few - %ld voters - many - %ld voters - other - %ld voters - - - plural.people_talking - - NSStringLocalizedFormatKey - %#@count_people_talking@ - count_people_talking - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 people talking - one - 1 people talking - few - %ld people talking - many - %ld people talking - other - %ld people talking - - - plural.count.following - - NSStringLocalizedFormatKey - %#@count_following@ - count_following - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 following - one - 1 following - few - %ld following - many - %ld following - other - %ld following - - - plural.count.follower - - NSStringLocalizedFormatKey - %#@count_follower@ - count_follower - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 followers - one - 1 follower - few - %ld followers - many - %ld followers - other - %ld followers - - - date.year.left - - NSStringLocalizedFormatKey - %#@count_year_left@ - count_year_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 years left - one - 1 year left - few - %ld years left - many - %ld years left - other - %ld years left - - - date.month.left - - NSStringLocalizedFormatKey - %#@count_month_left@ - count_month_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 months left - one - 1 months left - few - %ld months left - many - %ld months left - other - %ld months left - - - date.day.left - - NSStringLocalizedFormatKey - %#@count_day_left@ - count_day_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 days left - one - 1 day left - few - %ld days left - many - %ld days left - other - %ld days left - - - date.hour.left - - NSStringLocalizedFormatKey - %#@count_hour_left@ - count_hour_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 hours left - one - 1 hour left - few - %ld hours left - many - %ld hours left - other - %ld hours left - - - date.minute.left - - NSStringLocalizedFormatKey - %#@count_minute_left@ - count_minute_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 minutes left - one - 1 minute left - few - %ld minutes left - many - %ld minutes left - other - %ld minutes left - - - date.second.left - - NSStringLocalizedFormatKey - %#@count_second_left@ - count_second_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 seconds left - one - 1 second left - few - %ld seconds left - many - %ld seconds left - other - %ld seconds left - - - date.year.ago.abbr - - NSStringLocalizedFormatKey - %#@count_year_ago_abbr@ - count_year_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0y ago - one - 1y ago - few - %ldy ago - many - %ldy ago - other - %ldy ago - - - date.month.ago.abbr - - NSStringLocalizedFormatKey - %#@count_month_ago_abbr@ - count_month_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0M ago - one - 1M ago - few - %ldM ago - many - %ldM ago - other - %ldM ago - - - date.day.ago.abbr - - NSStringLocalizedFormatKey - %#@count_day_ago_abbr@ - count_day_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0d ago - one - 1d ago - few - %ldd ago - many - %ldd ago - other - %ldd ago - - - date.hour.ago.abbr - - NSStringLocalizedFormatKey - %#@count_hour_ago_abbr@ - count_hour_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0h ago - one - 1h ago - few - %ldh ago - many - %ldh ago - other - %ldh ago - - - date.minute.ago.abbr - - NSStringLocalizedFormatKey - %#@count_minute_ago_abbr@ - count_minute_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0m ago - one - 1m ago - few - %ldm ago - many - %ldm ago - other - %ldm ago - - - date.second.ago.abbr - - NSStringLocalizedFormatKey - %#@count_second_ago_abbr@ - count_second_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0s ago - one - 1s ago - few - %lds ago - many - %lds ago - other - %lds ago - - + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 following + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 followers + one + 1 follower + few + %ld followers + many + %ld followers + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 years left + one + 1 year left + few + %ld years left + many + %ld years left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 months left + one + 1 months left + few + %ld months left + many + %ld months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 days left + one + 1 day left + few + %ld days left + many + %ld days left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 hours left + one + 1 hour left + few + %ld hours left + many + %ld hours left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 minutes left + one + 1 minute left + few + %ld minutes left + many + %ld minutes left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 seconds left + one + 1 second left + few + %ld seconds left + many + %ld seconds left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0y ago + one + 1y ago + few + %ldy ago + many + %ldy ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0M ago + one + 1M ago + few + %ldM ago + many + %ldM ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0d ago + one + 1d ago + few + %ldd ago + many + %ldd ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0h ago + one + 1h ago + few + %ldh ago + many + %ldh ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0m ago + one + 1m ago + few + %ldm ago + many + %ldm ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0s ago + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + plural.filtered_notification_banner.subtitle + + NSStringLocalizedFormatKey + %#@number_of_requests@ + number_of_requests + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + One person you may know + other + %ld people you may know + + diff --git a/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict index a56e4b1e64..1038e2ea85 100644 --- a/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict +++ b/Localization/StringsConvertor/input/Base.lproj/Localizable.stringsdict @@ -2,72 +2,72 @@ - a11y.plural.count.unread.notification - - NSStringLocalizedFormatKey - %#@notification_count_unread_notification@ - notification_count_unread_notification - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - no unread notifications - one - 1 unread notification - few - %ld unread notifications - many - %ld unread notifications - other - %ld unread notifications - - - a11y.plural.count.input_limit_exceeds - - NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - - a11y.plural.count.input_limit_remains - - NSStringLocalizedFormatKey - Input limit remains %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no unread notifications + one + 1 unread notification + few + %ld unread notifications + many + %ld unread notifications + other + %ld unread notifications + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + a11y.plural.count.characters_left NSStringLocalizedFormatKey @@ -90,125 +90,125 @@ %ld characters left - plural.count.followed_by_and_mutual - - NSStringLocalizedFormatKey - %#@names@%#@count_mutual@ - names - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - other - - - count_mutual - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - Followed by %1$@ - one - Followed by %1$@, and another mutual - few - Followed by %1$@, and %ld mutuals - many - Followed by %1$@, and %ld mutuals - other - Followed by %1$@, and %ld mutuals - - - plural.count.metric_formatted.post - - NSStringLocalizedFormatKey - %@ %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - posts - one - post - few - posts - many - posts - other - posts - - - plural.count.media - - NSStringLocalizedFormatKey - %#@media_count@ - media_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 media - one - 1 media - few - %ld media - many - %ld media - other - %ld media - - - plural.count.post - - NSStringLocalizedFormatKey - %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 posts - one - 1 post - few - %ld posts - many - %ld posts - other - %ld posts - - + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@ + one + Followed by %1$@, and another mutual + few + Followed by %1$@, and %ld mutuals + many + Followed by %1$@, and %ld mutuals + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 media + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 posts + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + plural.count.favorite - - NSStringLocalizedFormatKey - %#@favorite_count@ - favorite_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 favorites - one - 1 favorite - few - %ld favorites - many - %ld favorites - other - %ld favorites - - + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + plural.count.reblog NSStringLocalizedFormatKey @@ -231,29 +231,29 @@ %ld reblogs - plural.count.reblog_a11y - - NSStringLocalizedFormatKey - %#@reblog_count@ - reblog_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 re-blogs - one - 1 re-blog - few - %ld re-blogs - many - %ld re-blogs - other - %ld re-blogs - - - plural.count.reply + plural.count.reblog_a11y + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 re-blogs + one + 1 re-blog + few + %ld re-blogs + many + %ld re-blogs + other + %ld re-blogs + + + plural.count.reply NSStringLocalizedFormatKey %#@reply_count@ @@ -275,379 +275,395 @@ %ld replies - plural.count.vote - - NSStringLocalizedFormatKey - %#@vote_count@ - vote_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 votes - one - 1 vote - few - %ld votes - many - %ld votes - other - %ld votes - - - plural.count.voter - - NSStringLocalizedFormatKey - %#@voter_count@ - voter_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 voters - one - 1 voter - few - %ld voters - many - %ld voters - other - %ld voters - - - plural.people_talking - - NSStringLocalizedFormatKey - %#@count_people_talking@ - count_people_talking - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 people talking - one - 1 people talking - few - %ld people talking - many - %ld people talking - other - %ld people talking - - - plural.count.following - - NSStringLocalizedFormatKey - %#@count_following@ - count_following - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 following - one - 1 following - few - %ld following - many - %ld following - other - %ld following - - - plural.count.follower - - NSStringLocalizedFormatKey - %#@count_follower@ - count_follower - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 followers - one - 1 follower - few - %ld followers - many - %ld followers - other - %ld followers - - - date.year.left - - NSStringLocalizedFormatKey - %#@count_year_left@ - count_year_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 years left - one - 1 year left - few - %ld years left - many - %ld years left - other - %ld years left - - - date.month.left - - NSStringLocalizedFormatKey - %#@count_month_left@ - count_month_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 months left - one - 1 months left - few - %ld months left - many - %ld months left - other - %ld months left - - - date.day.left - - NSStringLocalizedFormatKey - %#@count_day_left@ - count_day_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 days left - one - 1 day left - few - %ld days left - many - %ld days left - other - %ld days left - - - date.hour.left - - NSStringLocalizedFormatKey - %#@count_hour_left@ - count_hour_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 hours left - one - 1 hour left - few - %ld hours left - many - %ld hours left - other - %ld hours left - - - date.minute.left - - NSStringLocalizedFormatKey - %#@count_minute_left@ - count_minute_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 minutes left - one - 1 minute left - few - %ld minutes left - many - %ld minutes left - other - %ld minutes left - - - date.second.left - - NSStringLocalizedFormatKey - %#@count_second_left@ - count_second_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 seconds left - one - 1 second left - few - %ld seconds left - many - %ld seconds left - other - %ld seconds left - - - date.year.ago.abbr - - NSStringLocalizedFormatKey - %#@count_year_ago_abbr@ - count_year_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0y ago - one - 1y ago - few - %ldy ago - many - %ldy ago - other - %ldy ago - - - date.month.ago.abbr - - NSStringLocalizedFormatKey - %#@count_month_ago_abbr@ - count_month_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0M ago - one - 1M ago - few - %ldM ago - many - %ldM ago - other - %ldM ago - - - date.day.ago.abbr - - NSStringLocalizedFormatKey - %#@count_day_ago_abbr@ - count_day_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0d ago - one - 1d ago - few - %ldd ago - many - %ldd ago - other - %ldd ago - - - date.hour.ago.abbr - - NSStringLocalizedFormatKey - %#@count_hour_ago_abbr@ - count_hour_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0h ago - one - 1h ago - few - %ldh ago - many - %ldh ago - other - %ldh ago - - - date.minute.ago.abbr - - NSStringLocalizedFormatKey - %#@count_minute_ago_abbr@ - count_minute_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0m ago - one - 1m ago - few - %ldm ago - many - %ldm ago - other - %ldm ago - - - date.second.ago.abbr - - NSStringLocalizedFormatKey - %#@count_second_ago_abbr@ - count_second_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0s ago - one - 1s ago - few - %lds ago - many - %lds ago - other - %lds ago - - + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 following + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 followers + one + 1 follower + few + %ld followers + many + %ld followers + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 years left + one + 1 year left + few + %ld years left + many + %ld years left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 months left + one + 1 months left + few + %ld months left + many + %ld months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 days left + one + 1 day left + few + %ld days left + many + %ld days left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 hours left + one + 1 hour left + few + %ld hours left + many + %ld hours left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 minutes left + one + 1 minute left + few + %ld minutes left + many + %ld minutes left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 seconds left + one + 1 second left + few + %ld seconds left + many + %ld seconds left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0y ago + one + 1y ago + few + %ldy ago + many + %ldy ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0M ago + one + 1M ago + few + %ldM ago + many + %ldM ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0d ago + one + 1d ago + few + %ldd ago + many + %ldd ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0h ago + one + 1h ago + few + %ldh ago + many + %ldh ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0m ago + one + 1m ago + few + %ldm ago + many + %ldm ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0s ago + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + plural.filtered_notification_banner.subtitle + + NSStringLocalizedFormatKey + %#@number_of_requests@ + number_of_requests + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + One person you may know + other + %ld people you may know + + diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index f11b057a6a..a799483709 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -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": { diff --git a/Localization/app.json b/Localization/app.json index c84499439c..a799483709 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -752,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": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index ab6ac29406..8db2635d6d 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; D8318A892A4468DC00C0FB73 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; D8363B1529469CE200A74079 /* OnboardingNextView.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = OnboardingNextView.swift; sourceTree = ""; tabWidth = 4; }; + D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFilteringBannerTableViewCell.swift; sourceTree = ""; }; D84738D32BBD9ABE00ECD52B /* TimelineStatusPill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusPill.swift; sourceTree = ""; }; D84C099D2B0F9E33009E685E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D84C099F2B0F9E41009E685E /* Setup.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Setup.md; sourceTree = ""; }; @@ -799,6 +810,15 @@ D84C09A42B0F9E41009E685E /* Acknowledgments.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Acknowledgments.md; sourceTree = ""; }; D852C23B2AC5D02C00309232 /* AboutInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInstanceViewController.swift; sourceTree = ""; }; D852C23D2AC5D03300309232 /* InstanceRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceRulesViewController.swift; sourceTree = ""; }; + D85DF9682C481AF400A01408 /* NotificationPolicyFilterTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyFilterTableViewCell.swift; path = Policy/NotificationPolicyFilterTableViewCell.swift; sourceTree = ""; }; + D85DF9692C481AF700A01408 /* NotificationPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyViewController.swift; path = Policy/NotificationPolicyViewController.swift; sourceTree = ""; }; + D85DF96A2C481AF700A01408 /* NotificationPolicyHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotificationPolicyHeaderView.swift; path = Policy/NotificationPolicyHeaderView.swift; sourceTree = ""; }; + D85DF96E2C481B1100A01408 /* NotificationRequestsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRequestsTableViewController.swift; sourceTree = ""; }; + D85DF96F2C481B1100A01408 /* NotificationRequestTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationRequestTableViewCell.swift; sourceTree = ""; }; + D85DF9732C481B3500A01408 /* DataSourceFacade+Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Notifications.swift"; sourceTree = ""; }; + D85DF9752C4965A900A01408 /* NotificationRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestsViewModel.swift; sourceTree = ""; }; + D85DF9792C4E49A400A01408 /* NotificationRequestCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestCountView.swift; sourceTree = ""; }; + D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNotificationTimelineViewController.swift; sourceTree = ""; }; D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginView.swift; sourceTree = ""; }; D87BFC8C291EB81200FEE264 /* MastodonLoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginViewModel.swift; sourceTree = ""; }; D87BFC8E291EC26A00FEE264 /* MastodonLoginServerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonLoginServerTableViewCell.swift; sourceTree = ""; }; @@ -1510,6 +1530,7 @@ DB63F76E279A7D1100455B82 /* NotificationTableViewCell.swift */, DB63F774279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift */, D8A0729C2BEBA8D7001A4C7C /* AccountWarningNotificationCell.swift */, + D83B54F72C2AC2FA00D18A7B /* NotificationFilteringBannerTableViewCell.swift */, ); path = Cell; sourceTree = ""; @@ -1758,6 +1779,16 @@ path = Privacy; sourceTree = ""; }; + D80EC2602C2978CB009724A5 /* Notification Filtering */ = { + isa = PBXGroup; + children = ( + D85DF9682C481AF400A01408 /* NotificationPolicyFilterTableViewCell.swift */, + D85DF96A2C481AF700A01408 /* NotificationPolicyHeaderView.swift */, + D85DF9692C481AF700A01408 /* NotificationPolicyViewController.swift */, + ); + path = "Notification Filtering"; + sourceTree = ""; + }; D80F627E2B5C32E400877059 /* NotificationView */ = { isa = PBXGroup; children = ( @@ -1820,6 +1851,27 @@ path = Documentation; sourceTree = ""; }; + 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 = ""; + }; + D85DF97C2C50EF8700A01408 /* Account Notifications */ = { + isa = PBXGroup; + children = ( + D85DF97D2C50EFA700A01408 /* AccountNotificationTimelineViewController.swift */, + ); + path = "Account Notifications"; + sourceTree = ""; + }; 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; }; diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index acf985feef..4ca70d8ec9 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -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) diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift new file mode 100644 index 0000000000..7abca3694d --- /dev/null +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Notifications.swift @@ -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 + + } + +} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift index 76b19fa106..63ccabbf9e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+SearchHistory.swift @@ -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 } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift index aa71991d3b..1290953f72 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+NotificationTableViewCellDelegate.swift @@ -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 } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 4381498907..9db7ac73d7 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -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 } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift index d5e654dcad..bea5118e4e 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+UITableViewDelegate.swift @@ -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) + } } } } diff --git a/Mastodon/Protocol/Provider/DataSourceProvider.swift b/Mastodon/Protocol/Provider/DataSourceProvider.swift index fe886800e1..b6e1134fb3 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider.swift @@ -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?) } diff --git a/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift b/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift new file mode 100644 index 0000000000..2fd02878d7 --- /dev/null +++ b/Mastodon/Scene/Notification/Cell/NotificationFilteringBannerTableViewCell.swift @@ -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) + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift new file mode 100644 index 0000000000..76950c2139 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyFilterTableViewCell.swift @@ -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) + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyHeaderView.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyHeaderView.swift new file mode 100644 index 0000000000..84f2a07bb0 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyHeaderView.swift @@ -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) + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift new file mode 100644 index 0000000000..22f77332bf --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Policy/NotificationPolicyViewController.swift @@ -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? + 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(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() + + 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 + } + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift new file mode 100644 index 0000000000..e53886a4a5 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/Account Notifications/AccountNotificationTimelineViewController.swift @@ -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 + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestCountView.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestCountView.swift new file mode 100644 index 0000000000..4fbac07339 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestCountView.swift @@ -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) + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift new file mode 100644 index 0000000000..e84574820b --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestTableViewCell.swift @@ -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) + } + +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift new file mode 100644 index 0000000000..8befab7c75 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsTableViewController.swift @@ -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? + + 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(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() + 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() + 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() + 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) + } + } +} diff --git a/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsViewModel.swift b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsViewModel.swift new file mode 100644 index 0000000000..4ffe3be116 --- /dev/null +++ b/Mastodon/Scene/Notification/Notification Filtering/Requests/NotificationRequestsViewModel.swift @@ -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 + } +} diff --git a/Mastodon/Scene/Notification/NotificationItem.swift b/Mastodon/Scene/Notification/NotificationItem.swift index d5727e813e..72ca3d8454 100644 --- a/Mastodon/Scene/Notification/NotificationItem.swift +++ b/Mastodon/Scene/Notification/NotificationItem.swift @@ -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 diff --git a/Mastodon/Scene/Notification/NotificationSection.swift b/Mastodon/Scene/Notification/NotificationSection.swift index ac80faf974..cb630b904e 100644 --- a/Mastodon/Scene/Notification/NotificationSection.swift +++ b/Mastodon/Scene/Notification/NotificationSection.swift @@ -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 } } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift index e4bcc8a125..04a8820a4e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController+DataSourceProvider.swift @@ -33,7 +33,9 @@ extension NotificationTimelineViewController: DataSourceProvider { } }() return item - default: + case .filteredNotifications(let policy): + return DataSourceItem.notificationBanner(policy: policy) + case .bottomLoader, .feedLoader(_): return nil } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift index cd8a3158e9..60906a15a4 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewController.swift @@ -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() var observations = Set() - 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() + + 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() } } diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift index 9c5bf2949c..6070f48d41 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+Diffable.swift @@ -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() 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 }() diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 0999618569..7c6d8a347e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -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 ) diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift index 0df34434a4..2f8d9a6b5a 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel.swift @@ -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)) } } } diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift index 01ae0ab10f..b296bcd902 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView+Configuration.swift @@ -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) diff --git a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift index 341dd6e725..4da3ab81d8 100644 --- a/Mastodon/Scene/Notification/NotificationView/NotificationView.swift +++ b/Mastodon/Scene/Notification/NotificationView/NotificationView.swift @@ -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() { diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index df0b7d0fa5..5f55e15301 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -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 + } +} diff --git a/Mastodon/Scene/Notification/NotificationViewModel.swift b/Mastodon/Scene/Notification/NotificationViewModel.swift index b3a3316fd4..8b68367c19 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel.swift @@ -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() // 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. + } } } } diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift index ff1eab32ba..4891b175b9 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewController+DataSourceProvider.swift @@ -70,7 +70,7 @@ extension SearchResultViewController { provider: self, tag: tag ) - case .notification: + case .notification, .notificationBanner(_): assertionFailure() } // end switch diff --git a/Mastodon/Scene/Settings/General Settings/GeneralSettingToggleTableViewCell.swift b/Mastodon/Scene/Settings/General Settings/GeneralSettingToggleTableViewCell.swift index 5775fdcf5f..ab74574215 100644 --- a/Mastodon/Scene/Settings/General Settings/GeneralSettingToggleTableViewCell.swift +++ b/Mastodon/Scene/Settings/General Settings/GeneralSettingToggleTableViewCell.swift @@ -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) } diff --git a/Mastodon/Scene/Settings/Notification Settings/Cells/NotificationSettingTableViewToggleCell.swift b/Mastodon/Scene/Settings/Notification Settings/Cells/NotificationSettingTableViewToggleCell.swift index 6a8dfb0631..5bb8cff1f2 100644 --- a/Mastodon/Scene/Settings/Notification Settings/Cells/NotificationSettingTableViewToggleCell.swift +++ b/Mastodon/Scene/Settings/Notification Settings/Cells/NotificationSettingTableViewToggleCell.swift @@ -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) } diff --git a/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift b/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift index 11454d9e63..db47a7f75f 100644 --- a/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift +++ b/Mastodon/Scene/Settings/Shared/ToggleTableViewCell.swift @@ -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) diff --git a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift index beaa8d56b6..d91a98c28f 100644 --- a/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift +++ b/MastodonSDK/Sources/MastodonCore/DataController/FeedDataController.swift @@ -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 diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift index ff26e69b5c..9af896130d 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Notification.swift @@ -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 { + 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 { + 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") +} diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 3a8787b50b..ef5edbfdb2 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -895,6 +895,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") @@ -925,6 +933,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") @@ -1950,6 +1986,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 diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 5a6db9ba34..0e559e9395 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -304,11 +304,11 @@ uploaded to Mastodon."; "Scene.Following.Footer" = "Follows from other servers are not displayed."; "Scene.Following.Title" = "following"; "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"; @@ -316,6 +316,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"; @@ -328,6 +331,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."; @@ -621,4 +633,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"; \ No newline at end of file diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict index a56e4b1e64..1038e2ea85 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.stringsdict @@ -2,72 +2,72 @@ - a11y.plural.count.unread.notification - - NSStringLocalizedFormatKey - %#@notification_count_unread_notification@ - notification_count_unread_notification - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - no unread notifications - one - 1 unread notification - few - %ld unread notifications - many - %ld unread notifications - other - %ld unread notifications - - - a11y.plural.count.input_limit_exceeds - - NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - - a11y.plural.count.input_limit_remains - - NSStringLocalizedFormatKey - Input limit remains %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 characters - one - 1 character - few - %ld characters - many - %ld characters - other - %ld characters - - + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + no unread notifications + one + 1 unread notification + few + %ld unread notifications + many + %ld unread notifications + other + %ld unread notifications + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 characters + one + 1 character + few + %ld characters + many + %ld characters + other + %ld characters + + a11y.plural.count.characters_left NSStringLocalizedFormatKey @@ -90,125 +90,125 @@ %ld characters left - plural.count.followed_by_and_mutual - - NSStringLocalizedFormatKey - %#@names@%#@count_mutual@ - names - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - other - - - count_mutual - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - Followed by %1$@ - one - Followed by %1$@, and another mutual - few - Followed by %1$@, and %ld mutuals - many - Followed by %1$@, and %ld mutuals - other - Followed by %1$@, and %ld mutuals - - - plural.count.metric_formatted.post - - NSStringLocalizedFormatKey - %@ %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - posts - one - post - few - posts - many - posts - other - posts - - - plural.count.media - - NSStringLocalizedFormatKey - %#@media_count@ - media_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 media - one - 1 media - few - %ld media - many - %ld media - other - %ld media - - - plural.count.post - - NSStringLocalizedFormatKey - %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 posts - one - 1 post - few - %ld posts - many - %ld posts - other - %ld posts - - + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + Followed by %1$@ + one + Followed by %1$@, and another mutual + few + Followed by %1$@, and %ld mutuals + many + Followed by %1$@, and %ld mutuals + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + posts + one + post + few + posts + many + posts + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 media + one + 1 media + few + %ld media + many + %ld media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 posts + one + 1 post + few + %ld posts + many + %ld posts + other + %ld posts + + plural.count.favorite - - NSStringLocalizedFormatKey - %#@favorite_count@ - favorite_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 favorites - one - 1 favorite - few - %ld favorites - many - %ld favorites - other - %ld favorites - - + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 favorites + one + 1 favorite + few + %ld favorites + many + %ld favorites + other + %ld favorites + + plural.count.reblog NSStringLocalizedFormatKey @@ -231,29 +231,29 @@ %ld reblogs - plural.count.reblog_a11y - - NSStringLocalizedFormatKey - %#@reblog_count@ - reblog_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 re-blogs - one - 1 re-blog - few - %ld re-blogs - many - %ld re-blogs - other - %ld re-blogs - - - plural.count.reply + plural.count.reblog_a11y + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 re-blogs + one + 1 re-blog + few + %ld re-blogs + many + %ld re-blogs + other + %ld re-blogs + + + plural.count.reply NSStringLocalizedFormatKey %#@reply_count@ @@ -275,379 +275,395 @@ %ld replies - plural.count.vote - - NSStringLocalizedFormatKey - %#@vote_count@ - vote_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 votes - one - 1 vote - few - %ld votes - many - %ld votes - other - %ld votes - - - plural.count.voter - - NSStringLocalizedFormatKey - %#@voter_count@ - voter_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 voters - one - 1 voter - few - %ld voters - many - %ld voters - other - %ld voters - - - plural.people_talking - - NSStringLocalizedFormatKey - %#@count_people_talking@ - count_people_talking - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 people talking - one - 1 people talking - few - %ld people talking - many - %ld people talking - other - %ld people talking - - - plural.count.following - - NSStringLocalizedFormatKey - %#@count_following@ - count_following - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 following - one - 1 following - few - %ld following - many - %ld following - other - %ld following - - - plural.count.follower - - NSStringLocalizedFormatKey - %#@count_follower@ - count_follower - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 followers - one - 1 follower - few - %ld followers - many - %ld followers - other - %ld followers - - - date.year.left - - NSStringLocalizedFormatKey - %#@count_year_left@ - count_year_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 years left - one - 1 year left - few - %ld years left - many - %ld years left - other - %ld years left - - - date.month.left - - NSStringLocalizedFormatKey - %#@count_month_left@ - count_month_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 months left - one - 1 months left - few - %ld months left - many - %ld months left - other - %ld months left - - - date.day.left - - NSStringLocalizedFormatKey - %#@count_day_left@ - count_day_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 days left - one - 1 day left - few - %ld days left - many - %ld days left - other - %ld days left - - - date.hour.left - - NSStringLocalizedFormatKey - %#@count_hour_left@ - count_hour_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 hours left - one - 1 hour left - few - %ld hours left - many - %ld hours left - other - %ld hours left - - - date.minute.left - - NSStringLocalizedFormatKey - %#@count_minute_left@ - count_minute_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 minutes left - one - 1 minute left - few - %ld minutes left - many - %ld minutes left - other - %ld minutes left - - - date.second.left - - NSStringLocalizedFormatKey - %#@count_second_left@ - count_second_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0 seconds left - one - 1 second left - few - %ld seconds left - many - %ld seconds left - other - %ld seconds left - - - date.year.ago.abbr - - NSStringLocalizedFormatKey - %#@count_year_ago_abbr@ - count_year_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0y ago - one - 1y ago - few - %ldy ago - many - %ldy ago - other - %ldy ago - - - date.month.ago.abbr - - NSStringLocalizedFormatKey - %#@count_month_ago_abbr@ - count_month_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0M ago - one - 1M ago - few - %ldM ago - many - %ldM ago - other - %ldM ago - - - date.day.ago.abbr - - NSStringLocalizedFormatKey - %#@count_day_ago_abbr@ - count_day_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0d ago - one - 1d ago - few - %ldd ago - many - %ldd ago - other - %ldd ago - - - date.hour.ago.abbr - - NSStringLocalizedFormatKey - %#@count_hour_ago_abbr@ - count_hour_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0h ago - one - 1h ago - few - %ldh ago - many - %ldh ago - other - %ldh ago - - - date.minute.ago.abbr - - NSStringLocalizedFormatKey - %#@count_minute_ago_abbr@ - count_minute_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0m ago - one - 1m ago - few - %ldm ago - many - %ldm ago - other - %ldm ago - - - date.second.ago.abbr - - NSStringLocalizedFormatKey - %#@count_second_ago_abbr@ - count_second_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - zero - 0s ago - one - 1s ago - few - %lds ago - many - %lds ago - other - %lds ago - - + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 votes + one + 1 vote + few + %ld votes + many + %ld votes + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 voters + one + 1 voter + few + %ld voters + many + %ld voters + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 people talking + one + 1 people talking + few + %ld people talking + many + %ld people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 following + one + 1 following + few + %ld following + many + %ld following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 followers + one + 1 follower + few + %ld followers + many + %ld followers + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 years left + one + 1 year left + few + %ld years left + many + %ld years left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 months left + one + 1 months left + few + %ld months left + many + %ld months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 days left + one + 1 day left + few + %ld days left + many + %ld days left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 hours left + one + 1 hour left + few + %ld hours left + many + %ld hours left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 minutes left + one + 1 minute left + few + %ld minutes left + many + %ld minutes left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0 seconds left + one + 1 second left + few + %ld seconds left + many + %ld seconds left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0y ago + one + 1y ago + few + %ldy ago + many + %ldy ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0M ago + one + 1M ago + few + %ldM ago + many + %ldM ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0d ago + one + 1d ago + few + %ldd ago + many + %ldd ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0h ago + one + 1h ago + few + %ldh ago + many + %ldh ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0m ago + one + 1m ago + few + %ldm ago + many + %ldm ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + zero + 0s ago + one + 1s ago + few + %lds ago + many + %lds ago + other + %lds ago + + + plural.filtered_notification_banner.subtitle + + NSStringLocalizedFormatKey + %#@number_of_requests@ + number_of_requests + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + One person you may know + other + %ld people you may know + + diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict index 2b09ee0040..c97236f967 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/en.lproj/Localizable.stringsdict @@ -1,481 +1,497 @@ - + - - a11y.plural.count.unread.notification - - NSStringLocalizedFormatKey - %#@notification_count_unread_notification@ - notification_count_unread_notification - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 unread notification - other - %ld unread notifications - - - a11y.plural.count.input_limit_exceeds - - NSStringLocalizedFormatKey - Input limit exceeds %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 character - other - %ld characters - - - a11y.plural.count.input_limit_remains - - NSStringLocalizedFormatKey - Input limit remains %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 character - other - %ld characters - - - a11y.plural.count.characters_left - - NSStringLocalizedFormatKey - %#@character_count@ - character_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 character left - other - %ld characters left - - - plural.count.followed_by_and_mutual - - NSStringLocalizedFormatKey - %#@names@%#@count_mutual@ - names - - one - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - other - - - count_mutual - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - Followed by %1$@, and another mutual - other - Followed by %1$@, and %ld mutuals - - - plural.count.metric_formatted.post - - NSStringLocalizedFormatKey - %@ %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - post - other - posts - - - plural.count.media - - NSStringLocalizedFormatKey - %#@media_count@ - media_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 media - other - %ld media - - - plural.count.post - - NSStringLocalizedFormatKey - %#@post_count@ - post_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 post - other - %ld posts - - - plural.count.favorite - - NSStringLocalizedFormatKey - %#@favorite_count@ - favorite_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 favorite - other - %ld favorites - - - plural.count.reblog - - NSStringLocalizedFormatKey - %#@reblog_count@ - reblog_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 reblog - other - %ld reblogs - - - plural.count.reblog_a11y - - NSStringLocalizedFormatKey - %#@reblog_count@ - reblog_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 re-blog - other - %ld re-blogs - - - plural.count.reply - - NSStringLocalizedFormatKey - %#@reply_count@ - reply_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 reply - other - %ld replies - - - plural.count.vote - - NSStringLocalizedFormatKey - %#@vote_count@ - vote_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 vote - other - %ld votes - - - plural.count.voter - - NSStringLocalizedFormatKey - %#@voter_count@ - voter_count - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 voter - other - %ld voters - - - plural.people_talking - - NSStringLocalizedFormatKey - %#@count_people_talking@ - count_people_talking - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 people talking - other - %ld people talking - - - plural.count.following - - NSStringLocalizedFormatKey - %#@count_following@ - count_following - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 following - other - %ld following - - - plural.count.follower - - NSStringLocalizedFormatKey - %#@count_follower@ - count_follower - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 follower - other - %ld followers - - - date.year.left - - NSStringLocalizedFormatKey - %#@count_year_left@ - count_year_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 year left - other - %ld years left - - - date.month.left - - NSStringLocalizedFormatKey - %#@count_month_left@ - count_month_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 months left - other - %ld months left - - - date.day.left - - NSStringLocalizedFormatKey - %#@count_day_left@ - count_day_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 day left - other - %ld days left - - - date.hour.left - - NSStringLocalizedFormatKey - %#@count_hour_left@ - count_hour_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 hour left - other - %ld hours left - - - date.minute.left - - NSStringLocalizedFormatKey - %#@count_minute_left@ - count_minute_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 minute left - other - %ld minutes left - - - date.second.left - - NSStringLocalizedFormatKey - %#@count_second_left@ - count_second_left - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1 second left - other - %ld seconds left - - - date.year.ago.abbr - - NSStringLocalizedFormatKey - %#@count_year_ago_abbr@ - count_year_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1y ago - other - %ldy ago - - - date.month.ago.abbr - - NSStringLocalizedFormatKey - %#@count_month_ago_abbr@ - count_month_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1M ago - other - %ldM ago - - - date.day.ago.abbr - - NSStringLocalizedFormatKey - %#@count_day_ago_abbr@ - count_day_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1d ago - other - %ldd ago - - - date.hour.ago.abbr - - NSStringLocalizedFormatKey - %#@count_hour_ago_abbr@ - count_hour_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1h ago - other - %ldh ago - - - date.minute.ago.abbr - - NSStringLocalizedFormatKey - %#@count_minute_ago_abbr@ - count_minute_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1m ago - other - %ldm ago - - - date.second.ago.abbr - - NSStringLocalizedFormatKey - %#@count_second_ago_abbr@ - count_second_ago_abbr - - NSStringFormatSpecTypeKey - NSStringPluralRuleType - NSStringFormatValueTypeKey - ld - one - 1s ago - other - %lds ago - - - + + a11y.plural.count.unread.notification + + NSStringLocalizedFormatKey + %#@notification_count_unread_notification@ + notification_count_unread_notification + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 unread notification + other + %ld unread notifications + + + a11y.plural.count.input_limit_exceeds + + NSStringLocalizedFormatKey + Input limit exceeds %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + a11y.plural.count.input_limit_remains + + NSStringLocalizedFormatKey + Input limit remains %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character + other + %ld characters + + + a11y.plural.count.characters_left + + NSStringLocalizedFormatKey + %#@character_count@ + character_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 character left + other + %ld characters left + + + plural.count.followed_by_and_mutual + + NSStringLocalizedFormatKey + %#@names@%#@count_mutual@ + names + + one + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + other + + + count_mutual + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + Followed by %1$@, and another mutual + other + Followed by %1$@, and %ld mutuals + + + plural.count.metric_formatted.post + + NSStringLocalizedFormatKey + %@ %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + post + other + posts + + + plural.count.media + + NSStringLocalizedFormatKey + %#@media_count@ + media_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 media + other + %ld media + + + plural.count.post + + NSStringLocalizedFormatKey + %#@post_count@ + post_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 post + other + %ld posts + + + plural.count.favorite + + NSStringLocalizedFormatKey + %#@favorite_count@ + favorite_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 favorite + other + %ld favorites + + + plural.count.reblog + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reblog + other + %ld reblogs + + + plural.count.reblog_a11y + + NSStringLocalizedFormatKey + %#@reblog_count@ + reblog_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 re-blog + other + %ld re-blogs + + + plural.count.reply + + NSStringLocalizedFormatKey + %#@reply_count@ + reply_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 reply + other + %ld replies + + + plural.count.vote + + NSStringLocalizedFormatKey + %#@vote_count@ + vote_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 vote + other + %ld votes + + + plural.count.voter + + NSStringLocalizedFormatKey + %#@voter_count@ + voter_count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 voter + other + %ld voters + + + plural.people_talking + + NSStringLocalizedFormatKey + %#@count_people_talking@ + count_people_talking + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 people talking + other + %ld people talking + + + plural.count.following + + NSStringLocalizedFormatKey + %#@count_following@ + count_following + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 following + other + %ld following + + + plural.count.follower + + NSStringLocalizedFormatKey + %#@count_follower@ + count_follower + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 follower + other + %ld followers + + + date.year.left + + NSStringLocalizedFormatKey + %#@count_year_left@ + count_year_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 year left + other + %ld years left + + + date.month.left + + NSStringLocalizedFormatKey + %#@count_month_left@ + count_month_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 months left + other + %ld months left + + + date.day.left + + NSStringLocalizedFormatKey + %#@count_day_left@ + count_day_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 day left + other + %ld days left + + + date.hour.left + + NSStringLocalizedFormatKey + %#@count_hour_left@ + count_hour_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 hour left + other + %ld hours left + + + date.minute.left + + NSStringLocalizedFormatKey + %#@count_minute_left@ + count_minute_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 minute left + other + %ld minutes left + + + date.second.left + + NSStringLocalizedFormatKey + %#@count_second_left@ + count_second_left + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1 second left + other + %ld seconds left + + + date.year.ago.abbr + + NSStringLocalizedFormatKey + %#@count_year_ago_abbr@ + count_year_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1y ago + other + %ldy ago + + + date.month.ago.abbr + + NSStringLocalizedFormatKey + %#@count_month_ago_abbr@ + count_month_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1M ago + other + %ldM ago + + + date.day.ago.abbr + + NSStringLocalizedFormatKey + %#@count_day_ago_abbr@ + count_day_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1d ago + other + %ldd ago + + + date.hour.ago.abbr + + NSStringLocalizedFormatKey + %#@count_hour_ago_abbr@ + count_hour_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1h ago + other + %ldh ago + + + date.minute.ago.abbr + + NSStringLocalizedFormatKey + %#@count_minute_ago_abbr@ + count_minute_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1m ago + other + %ldm ago + + + date.second.ago.abbr + + NSStringLocalizedFormatKey + %#@count_second_ago_abbr@ + count_second_ago_abbr + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + 1s ago + other + %lds ago + + + plural.filtered_notification_banner.subtitle + + NSStringLocalizedFormatKey + %#@number_of_requests@ + number_of_requests + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + ld + one + One person you may know + other + %ld people you may know + + + diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift index f70caaa1dc..7244c6e9c6 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Notifications.swift @@ -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 { + 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 { + 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) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 1103322879..a733a07051 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -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) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift new file mode 100644 index 0000000000..68755e9b60 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationPolicy.swift @@ -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" + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationRequest.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationRequest.swift new file mode 100644 index 0000000000..2f6188b7ea --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+NotificationRequest.swift @@ -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" + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift index 33208fcc66..df53767b38 100644 --- a/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift +++ b/MastodonSDK/Sources/MastodonSDK/MastodonFeed.swift @@ -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 diff --git a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift index f93d8af1b9..ba3ec63cd2 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/Query.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift @@ -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 { } diff --git a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift index eba7e1672f..2de73dc5b5 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Control/ActionToolbarContainer.swift @@ -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