diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-06-17 12:37:14 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-17 02:37:14 -0700 |
commit | 21989b558bd074bf84ac08c174d7a411fda1ffb7 (patch) | |
tree | f5f28510cf5a592b83bcfc581a57e992823eb402 | |
parent | 7dc6bb57a6666db3e507630c13448487acceadc5 (diff) | |
download | voidsky-21989b558bd074bf84ac08c174d7a411fda1ffb7.tar.zst |
Granular notification settings (#8484)
* add mockup screen * add notification index screen * add redirect screen * upgrade sdk * new icons * add new screens * make router typesafe, finish adding screens * add routes to go server * load settings * push notif settings * improve web * fix lockfile lint * no $type on preferences * prompt to enable push notifications * fix reply prefs * space out options * fix copy error * Update RepostsOnRepostsNotificationSettings.tsx * only send minimal diff to putPrefs * fix yarn.lock * Update Navigation.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Settings/NotificationSettings/index.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add description to `syncOthers` --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
34 files changed, 1434 insertions, 287 deletions
diff --git a/assets/icons/bellRinging_stroke2_corner0_rounded.svg b/assets/icons/bellRinging_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..d49a59a01 --- /dev/null +++ b/assets/icons/bellRinging_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.5 9.5 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z"/></svg> diff --git a/assets/icons/likeRepost_stroke2_corner2_rounded.svg b/assets/icons/likeRepost_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..f5d2da35b --- /dev/null +++ b/assets/icons/likeRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3.92 19v-4.153a1 1 0 0 1 1-1H9l.103.005a1 1 0 0 1 0 1.99L9 15.847H7.285c.854.737 1.784 1.38 2.631 1.9.702.431 1.329.769 1.78.997l.291.143a25.6 25.6 0 0 0 3.67-2.326c2.144-1.642 4.073-3.756 4.315-6.023a1 1 0 0 1 1.988.212c-.336 3.154-2.89 5.717-5.086 7.398a27.6 27.6 0 0 1-4.34 2.704l-.078.038-.021.01-.007.003-.002.001-.001.001a1 1 0 0 1-.827.01v0h-.002l-.004-.002-.013-.006q-.016-.006-.045-.02l-.162-.075a27 27 0 0 1-2.503-1.361 22 22 0 0 1-2.95-2.143V19a1 1 0 0 1-2 0ZM2 10c0-2.214.696-3.971 1.833-5.184A5.7 5.7 0 0 1 8 3a7.1 7.1 0 0 1 4 1.228A7.1 7.1 0 0 1 16 3a5.68 5.68 0 0 1 3.469 1.185l.031-1.702a1 1 0 0 1 2 .035l-.081 4.5a1 1 0 0 1-1 .983H16.5a1 1 0 1 1 0-2h2.02A3.68 3.68 0 0 0 16 5a5.12 5.12 0 0 0-3.11 1.053 3 3 0 0 0-.155.129l-.029.025v.002l-.003.002-.072.064a1 1 0 0 1-1.338-.068l-.028-.025a3 3 0 0 0-.155-.13A5.12 5.12 0 0 0 8 5c-.982 0-1.965.392-2.708 1.185C4.554 6.97 4 8.214 4 10q0 .507.099 1.002l.075.328.02.1a1 1 0 0 1-1.925.5l-.03-.097-.102-.446A7 7 0 0 1 2 10Z"/></svg> diff --git a/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..ebcf89b46 --- /dev/null +++ b/assets/icons/phoneHaptic_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M16 6a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6ZM2.87 7.225a1 1 0 0 1 1.337 1.482L3.155 9.759a.546.546 0 0 0-.05.714l.119.173c.52.827.52 1.88 0 2.707l-.12.174a.546.546 0 0 0 .051.714l1.052 1.052.069.076a1 1 0 0 1-1.407 1.406l-.076-.068-1.052-1.052a2.55 2.55 0 0 1-.237-3.328l.048-.075a.55.55 0 0 0 0-.504l-.048-.075a2.55 2.55 0 0 1 .237-3.328l1.052-1.052.076-.068Zm16.923.068a1 1 0 0 1 1.338-.068l.076.068 1.052 1.052.16.174c.696.837.78 2.03.209 2.958l-.133.196a.55.55 0 0 0 0 .654l.133.196a2.55 2.55 0 0 1-.21 2.958l-.16.174-1.05 1.052a1 1 0 1 1-1.415-1.414l1.052-1.052.064-.077a.55.55 0 0 0 .04-.552l-.053-.085a2.545 2.545 0 0 1 0-3.054l.052-.085a.55.55 0 0 0-.039-.552l-.064-.077-1.052-1.052-.068-.076a1 1 0 0 1 .068-1.338ZM13 6l.103.005a1 1 0 0 1 0 1.99L13 8h-2a1 1 0 1 1 0-2h2Zm5 12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12Z"/></svg> diff --git a/assets/icons/repostRepost_stroke2_corner2_rounded.svg b/assets/icons/repostRepost_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..caec8c102 --- /dev/null +++ b/assets/icons/repostRepost_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg> diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index f419212cc..ef796920d 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -278,6 +278,17 @@ func serve(cctx *cli.Context) error { e.GET("/settings/content-and-media", server.WebGeneric) e.GET("/settings/interests", server.WebGeneric) e.GET("/settings/about", server.WebGeneric) + e.GET("/settings/notifications", server.WebGeneric) + e.GET("/settings/notifications/replies", server.WebGeneric) + e.GET("/settings/notifications/mentions", server.WebGeneric) + e.GET("/settings/notifications/quotes", server.WebGeneric) + e.GET("/settings/notifications/likes", server.WebGeneric) + e.GET("/settings/notifications/reposts", server.WebGeneric) + e.GET("/settings/notifications/new-followers", server.WebGeneric) + e.GET("/settings/notifications/likes-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/reposts-on-reposts", server.WebGeneric) + e.GET("/settings/notifications/activity", server.WebGeneric) + e.GET("/settings/notifications/miscellaneous", server.WebGeneric) e.GET("/settings/app-icon", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric) diff --git a/package.json b/package.json index 691a6e94d..fe3855414 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,7 @@ "zod": "^3.20.2" }, "devDependencies": { - "@atproto/dev-env": "^0.3.133", + "@atproto/dev-env": "^0.3.142", "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/runtime": "^7.26.0", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 2f26c0971..3bf1ace85 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -90,11 +90,10 @@ import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' +import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' -import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' -import {SettingsInterests} from '#/screens/Settings/SettingsInterests' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { StarterPackScreen, @@ -110,6 +109,17 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' +import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' +import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' +import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' +import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' +import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' +import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' +import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' +import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' +import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' +import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' +import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -381,6 +391,83 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> <Stack.Screen + name="NotificationSettings" + getComponent={() => NotificationSettingsScreen} + options={{title: title(msg`Notification settings`), requireAuth: true}} + /> + <Stack.Screen + name="ReplyNotificationSettings" + getComponent={() => ReplyNotificationSettingsScreen} + options={{ + title: title(msg`Reply notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="MentionNotificationSettings" + getComponent={() => MentionNotificationSettingsScreen} + options={{ + title: title(msg`Mention notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="QuoteNotificationSettings" + getComponent={() => QuoteNotificationSettingsScreen} + options={{ + title: title(msg`Quote notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="LikeNotificationSettings" + getComponent={() => LikeNotificationSettingsScreen} + options={{ + title: title(msg`Like notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="RepostNotificationSettings" + getComponent={() => RepostNotificationSettingsScreen} + options={{ + title: title(msg`Repost notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="NewFollowerNotificationSettings" + getComponent={() => NewFollowerNotificationSettingsScreen} + options={{ + title: title(msg`New follower notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="LikesOnRepostsNotificationSettings" + getComponent={() => LikesOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Likes on your reposts notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="RepostsOnRepostsNotificationSettings" + getComponent={() => RepostsOnRepostsNotificationSettingsScreen} + options={{ + title: title(msg`Reposts on your reposts notifications`), + requireAuth: true, + }} + /> + <Stack.Screen + name="MiscellaneousNotificationSettings" + getComponent={() => MiscellaneousNotificationSettingsScreen} + options={{ + title: title(msg`Miscellaneous notifications`), + requireAuth: true, + }} + /> + <Stack.Screen name="ContentAndMediaSettings" getComponent={() => ContentAndMediaSettingsScreen} options={{ @@ -389,8 +476,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { }} /> <Stack.Screen - name="SettingsInterests" - getComponent={() => SettingsInterests} + name="InterestsSettings" + getComponent={() => InterestsSettingsScreen} options={{ title: title(msg`Your interests`), requireAuth: true, @@ -438,8 +525,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { options={{title: title(msg`Chat request inbox`), requireAuth: true}} /> <Stack.Screen - name="NotificationSettings" - getComponent={() => NotificationSettingsScreen} + name="LegacyNotificationSettings" + getComponent={() => LegacyNotificationSettingsScreen} options={{title: title(msg`Notification settings`), requireAuth: true}} /> <Stack.Screen diff --git a/src/components/icons/BellRinging.tsx b/src/components/icons/BellRinging.tsx new file mode 100644 index 000000000..b174fcedc --- /dev/null +++ b/src/components/icons/BellRinging.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a7.854 7.854 0 0 1 7.785 6.815l1.055 7.92.018.224a2 2 0 0 1-2 2.041h-2.215c-.904 1.747-2.605 3-4.643 3s-3.739-1.253-4.643-3H5.142a2 2 0 0 1-1.982-2.265l1.056-7.92.057-.363A7.854 7.854 0 0 1 12 2ZM9.78 19c.609.637 1.398 1 2.22 1s1.611-.363 2.22-1H9.78ZM12 4a5.854 5.854 0 0 0-5.76 4.81l-.041.27L5.142 17h13.716l-1.056-7.92A5.854 5.854 0 0 0 12 4ZM2.718 7.464a1 1 0 1 1-1.953-.427l1.953.427Zm20.518-.427a1 1 0 0 1-1.954.427l1.954-.427ZM3.193 2.105a1 1 0 0 1 1.531 1.287 9.47 9.47 0 0 0-2.006 4.072L.765 7.037a11.46 11.46 0 0 1 2.428-4.932Zm16.205-.123a1 1 0 0 1 1.34.047l.069.076.217.265a11.46 11.46 0 0 1 2.212 4.667l-.978.213-.976.214a9.46 9.46 0 0 0-1.826-3.853l-.18-.22-.062-.081a1 1 0 0 1 .184-1.328Z', +}) diff --git a/src/components/icons/Heart2.tsx b/src/components/icons/Heart2.tsx index 07f5a1d2c..9c9b0be5b 100644 --- a/src/components/icons/Heart2.tsx +++ b/src/components/icons/Heart2.tsx @@ -7,3 +7,7 @@ export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({ export const Heart2_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z', }) + +export const LikeRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M3.92 19v-4.153a1 1 0 0 1 1-1H9l.103.005a1 1 0 0 1 0 1.99L9 15.847H7.285c.854.737 1.784 1.38 2.631 1.9.702.431 1.329.769 1.78.997q.162.08.291.143a25.561 25.561 0 0 0 3.67-2.326c2.144-1.642 4.073-3.756 4.315-6.023a1 1 0 0 1 1.988.212c-.336 3.154-2.89 5.717-5.086 7.398a27.6 27.6 0 0 1-4.34 2.704l-.078.038-.021.01-.007.003-.002.001-.001.001a1 1 0 0 1-.827.01v0h-.002l-.004-.002-.013-.006q-.016-.006-.045-.02l-.162-.075a27.39 27.39 0 0 1-2.503-1.361 22 22 0 0 1-2.95-2.143V19a1 1 0 0 1-2 0ZM2 10c0-2.214.696-3.971 1.833-5.184A5.7 5.7 0 0 1 8 3a7.1 7.1 0 0 1 4 1.228A7.117 7.117 0 0 1 16 3c1.231 0 2.452.402 3.469 1.185l.031-1.702a1 1 0 0 1 2 .035l-.081 4.5a1 1 0 0 1-1 .983H16.5a1 1 0 1 1 0-2h2.02A3.68 3.68 0 0 0 16 5a5.12 5.12 0 0 0-3.11 1.053 3 3 0 0 0-.155.129l-.029.025v.002l-.003.002-.072.064a1 1 0 0 1-1.338-.068l-.028-.025a3 3 0 0 0-.155-.13A5.119 5.119 0 0 0 8 5c-.982 0-1.965.392-2.708 1.185C4.554 6.97 4 8.214 4 10q0 .507.099 1.002l.075.328.02.1a1 1 0 0 1-1.925.5l-.03-.097-.102-.446A7 7 0 0 1 2 10Z', +}) diff --git a/src/components/icons/Phone.tsx b/src/components/icons/Phone.tsx index 62000a1e5..8bfabc2a6 100644 --- a/src/components/icons/Phone.tsx +++ b/src/components/icons/Phone.tsx @@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE' export const Phone_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M5 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H8Zm2 2a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z', }) + +export const PhoneHaptic_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M16 6a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6ZM2.87 7.225a1 1 0 0 1 1.337 1.482L3.155 9.759a.546.546 0 0 0-.05.714l.119.173c.52.827.52 1.88 0 2.707l-.12.174a.546.546 0 0 0 .051.714l1.052 1.052.069.076a1 1 0 0 1-1.407 1.406l-.076-.068-1.052-1.052a2.55 2.55 0 0 1-.237-3.328l.048-.075a.55.55 0 0 0 0-.504l-.048-.075a2.55 2.55 0 0 1 .237-3.328l1.052-1.052.076-.068Zm16.923.068a1 1 0 0 1 1.338-.068l.076.068 1.052 1.052.16.174c.696.837.78 2.03.209 2.958l-.133.196a.55.55 0 0 0 0 .654l.133.196a2.55 2.55 0 0 1-.21 2.958l-.16.174-1.05 1.052a1 1 0 1 1-1.415-1.414l1.052-1.052.064-.077a.55.55 0 0 0 .04-.552l-.053-.085a2.545 2.545 0 0 1 0-3.054l.052-.085a.55.55 0 0 0-.039-.552l-.064-.077-1.052-1.052-.068-.076a1 1 0 0 1 .068-1.338ZM13 6l.103.005a1 1 0 0 1 0 1.99L13 8h-2a1 1 0 1 1 0-2h2Zm5 12a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v12Z', +}) diff --git a/src/components/icons/Repost.tsx b/src/components/icons/Repost.tsx index 01214bca7..abf2c8ac2 100644 --- a/src/components/icons/Repost.tsx +++ b/src/components/icons/Repost.tsx @@ -11,3 +11,7 @@ export const Repost_Stroke2_Corner2_Rounded = createSinglePathSVG({ export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({ path: 'M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z', }) + +export const RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M6.043 14.293a1 1 0 1 1 1.414 1.414L5.164 18l2.293 2.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47Zm6.22 0a1 1 0 0 1 1.414 1.414L12.384 17H18a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3h-5.616l1.293 1.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47ZM3 11V8a3 3 0 0 1 3-3h5.586l-1.293-1.293-.068-.076a1 1 0 0 1 1.406-1.406l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L11.586 7H6a1 1 0 0 0-1 1v3a1 1 0 1 1-2 0Zm13.543-8.707a1 1 0 0 1 1.338-.068l.076.068 2.47 2.47.12.133a1.75 1.75 0 0 1 0 2.209l-.12.132-2.47 2.47a1 1 0 1 1-1.414-1.414L18.836 6l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z', +}) diff --git a/src/lib/routes/router.ts b/src/lib/routes/router.ts index ba76b1bda..c74192f29 100644 --- a/src/lib/routes/router.ts +++ b/src/lib/routes/router.ts @@ -1,8 +1,8 @@ import {type Route, type RouteParams} from './types' -export class Router { +export class Router<T extends Record<string, any>> { routes: [string, Route][] = [] - constructor(description: Record<string, string | string[]>) { + constructor(description: Record<keyof T, string | string[]>) { for (const [screen, pattern] of Object.entries(description)) { if (typeof pattern === 'string') { this.routes.push([screen, createRoute(pattern)]) @@ -14,7 +14,7 @@ export class Router { } } - matchName(name: string): Route | undefined { + matchName(name: keyof T | (string & {})): Route | undefined { for (const [screenName, route] of this.routes) { if (screenName === name) { return route diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index f58742390..c92be34c2 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -52,7 +52,18 @@ export type CommonNavigatorParams = { AccountSettings: undefined PrivacyAndSecuritySettings: undefined ContentAndMediaSettings: undefined - SettingsInterests: undefined + NotificationSettings: undefined + ReplyNotificationSettings: undefined + MentionNotificationSettings: undefined + QuoteNotificationSettings: undefined + LikeNotificationSettings: undefined + RepostNotificationSettings: undefined + NewFollowerNotificationSettings: undefined + LikesOnRepostsNotificationSettings: undefined + RepostsOnRepostsNotificationSettings: undefined + ActivityNotificationSettings: undefined + MiscellaneousNotificationSettings: undefined + InterestsSettings: undefined AboutSettings: undefined AppIconSettings: undefined Search: {q?: string} @@ -61,7 +72,7 @@ export type CommonNavigatorParams = { MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesSettings: undefined MessagesInbox: undefined - NotificationSettings: undefined + LegacyNotificationSettings: undefined Feeds: undefined Start: {name: string; rkey: string} StarterPack: {name: string; rkey: string; new?: boolean} @@ -104,8 +115,6 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined Notifications: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -118,15 +127,8 @@ export type AllNavigatorParams = CommonNavigatorParams & { NotificationsTab: undefined Notifications: undefined MyProfileTab: undefined - Hashtag: {tag: string; author?: string} - Topic: {topic: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} - Start: {name: string; rkey: string} - StarterPack: {name: string; rkey: string; new?: boolean} - StarterPackShort: {code: string} - StarterPackWizard: undefined - StarterPackEdit: {rkey?: string} } // NOTE diff --git a/src/routes.ts b/src/routes.ts index 60bb65dd5..b66a0ae53 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,11 +1,17 @@ import {Router} from '#/lib/routes/router' +import {type FlatNavigatorParams} from './lib/routes/types' -export const router = new Router({ +type AllNavigatableRoutes = Omit< + FlatNavigatorParams, + 'NotFound' | 'SharedPreferencesTester' +> + +export const router = new Router<AllNavigatableRoutes>({ Home: '/', Search: '/search', Feeds: '/feeds', Notifications: '/notifications', - NotificationSettings: '/notifications/settings', + LegacyNotificationSettings: '/notifications/settings', Settings: '/settings', Lists: '/lists', // moderation @@ -42,13 +48,25 @@ export const router = new Router({ AccessibilitySettings: '/settings/accessibility', AppearanceSettings: '/settings/appearance', SavedFeeds: '/settings/saved-feeds', - // new settings AccountSettings: '/settings/account', PrivacyAndSecuritySettings: '/settings/privacy-and-security', ContentAndMediaSettings: '/settings/content-and-media', - SettingsInterests: '/settings/interests', + InterestsSettings: '/settings/interests', AboutSettings: '/settings/about', AppIconSettings: '/settings/app-icon', + NotificationSettings: '/settings/notifications', + ReplyNotificationSettings: '/settings/notifications/replies', + MentionNotificationSettings: '/settings/notifications/mentions', + QuoteNotificationSettings: '/settings/notifications/quotes', + LikeNotificationSettings: '/settings/notifications/likes', + RepostNotificationSettings: '/settings/notifications/reposts', + NewFollowerNotificationSettings: '/settings/notifications/new-followers', + LikesOnRepostsNotificationSettings: + '/settings/notifications/likes-on-reposts', + RepostsOnRepostsNotificationSettings: + '/settings/notifications/reposts-on-reposts', + ActivityNotificationSettings: '/settings/notifications/activity', + MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', // support Support: '/support', PrivacyPolicy: '/support/privacy', diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index f37e7a9ba..0b8c88b9d 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -2,9 +2,9 @@ import {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {CommonNavigatorParams} from '#/lib/routes/types' +import {type CommonNavigatorParams} from '#/lib/routes/types' import {isNative} from '#/platform/detection' import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery} from '#/state/queries/profile' diff --git a/src/screens/Settings/SettingsInterests.tsx b/src/screens/Settings/InterestsSettings.tsx index 42259e9b6..746315f7b 100644 --- a/src/screens/Settings/SettingsInterests.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -2,9 +2,11 @@ import {useMemo, useState} from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' import debounce from 'lodash.debounce' +import {type CommonNavigatorParams} from '#/lib/routes/types' import { preferencesQueryKey, usePreferencesQuery, @@ -24,7 +26,8 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' -export function SettingsInterests() { +type Props = NativeStackScreenProps<CommonNavigatorParams, 'InterestsSettings'> +export function InterestsSettingsScreen({}: Props) { const t = useTheme() const gutters = useGutters(['base']) const {data: preferences} = usePreferencesQuery() diff --git a/src/screens/Settings/LegacyNotificationSettings.tsx b/src/screens/Settings/LegacyNotificationSettings.tsx new file mode 100644 index 000000000..a9ef5d983 --- /dev/null +++ b/src/screens/Settings/LegacyNotificationSettings.tsx @@ -0,0 +1,21 @@ +import {useCallback} from 'react' +import {useFocusEffect} from '@react-navigation/native' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LegacyNotificationSettings' +> +export function LegacyNotificationSettingsScreen({navigation}: Props) { + useFocusEffect( + useCallback(() => { + navigation.replace('NotificationSettings') + }, [navigation]), + ) + + return null +} diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx deleted file mode 100644 index ebb230c2c..000000000 --- a/src/screens/Settings/NotificationSettings.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import {Text} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' -import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings' -import {atoms as a} from '#/alf' -import {Admonition} from '#/components/Admonition' -import {Error} from '#/components/Error' -import * as Toggle from '#/components/forms/Toggle' -import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' -import * as Layout from '#/components/Layout' -import {Loader} from '#/components/Loader' -import * as SettingsList from './components/SettingsList' - -type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'> -export function NotificationSettingsScreen({}: Props) { - const {_} = useLingui() - - const { - data, - isError: isQueryError, - refetch, - } = useNotificationFeedQuery({ - filter: 'all', - }) - const serverPriority = data?.pages.at(0)?.priority - - const { - mutate: onChangePriority, - isPending: isMutationPending, - variables, - } = useNotificationSettingsMutation() - - const priority = isMutationPending - ? variables[0] === 'enabled' - : serverPriority - - return ( - <Layout.Screen> - <Layout.Header.Outer> - <Layout.Header.BackButton /> - <Layout.Header.Content> - <Layout.Header.TitleText> - <Trans>Notification Settings</Trans> - </Layout.Header.TitleText> - </Layout.Header.Content> - <Layout.Header.Slot /> - </Layout.Header.Outer> - <Layout.Content> - {isQueryError ? ( - <Error - title={_(msg`Oops!`)} - message={_(msg`Something went wrong!`)} - onRetry={refetch} - sideBorders={false} - /> - ) : ( - <SettingsList.Container> - <SettingsList.Group> - <SettingsList.ItemIcon icon={BeakerIcon} /> - <SettingsList.ItemText> - <Trans>Notification filters</Trans> - </SettingsList.ItemText> - <Toggle.Group - label={_(msg`Priority notifications`)} - type="checkbox" - values={priority ? ['enabled'] : []} - onChange={onChangePriority} - disabled={typeof priority !== 'boolean' || isMutationPending}> - <Toggle.Item - name="enabled" - label={_(msg`Enable priority notifications`)} - style={[a.flex_1, a.justify_between]}> - <Toggle.LabelText> - <Trans>Enable priority notifications</Trans> - </Toggle.LabelText> - {!data ? <Loader size="md" /> : <Toggle.Platform />} - </Toggle.Item> - </Toggle.Group> - </SettingsList.Group> - <SettingsList.Item> - <Admonition type="warning" style={[a.flex_1]}> - <Trans> - <Text style={[a.font_bold]}>Experimental:</Text> When this - preference is enabled, you'll only receive reply and quote - notifications from users you follow. We'll continue to add - more controls here over time. - </Trans> - </Admonition> - </SettingsList.Item> - </SettingsList.Container> - )} - </Layout.Content> - </Layout.Screen> - ) -} diff --git a/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx new file mode 100644 index 000000000..f726ab558 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Heart2_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikeNotificationSettings' +> +export function LikeNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={HeartIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Likes</Trans>} + subtitleText={ + <Trans>Get notifications when people like your posts.</Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls name="like" preference={preferences?.like} /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx new file mode 100644 index 000000000..08a05d468 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx @@ -0,0 +1,65 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon} from '#/components/icons/Heart2' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'LikesOnRepostsNotificationSettings' +> +export function LikesOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={LikeRepostIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Likes on your reposts</Trans>} + subtitleText={ + <Trans> + Get notifications when people like posts that you've reposted. + </Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="likeViaRepost" + preference={preferences?.likeViaRepost} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx new file mode 100644 index 000000000..0a770157e --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MentionNotificationSettings' +> +export function MentionNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={AtIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Mentions</Trans>} + subtitleText={ + <Trans>Get notifications when people mention you.</Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="mention" + preference={preferences?.mention} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx new file mode 100644 index 000000000..a0fe65ecf --- /dev/null +++ b/src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx @@ -0,0 +1,68 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'MiscellaneousNotificationSettings' +> +export function MiscellaneousNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={ShapesIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Everything else</Trans>} + subtitleText={ + <Trans> + Notifications for everything else, such as when someone joins + via one of your starter packs. + </Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="starterpackJoined" + preference={preferences?.starterpackJoined} + syncOthers={['verified', 'unverified']} + allowDisableInApp={false} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx new file mode 100644 index 000000000..dd603a52f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'NewFollowerNotificationSettings' +> +export function NewFollowerNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={PersonPlusIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>New followers</Trans>} + subtitleText={ + <Trans>Get notifications when people follow you.</Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="follow" + preference={preferences?.follow} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx new file mode 100644 index 000000000..afb3df90f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx @@ -0,0 +1,60 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'QuoteNotificationSettings' +> +export function QuoteNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={CloseQuoteIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Quotes</Trans>} + subtitleText={ + <Trans>Get notifications when people quote your posts.</Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls name="quote" preference={preferences?.quote} /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx new file mode 100644 index 000000000..b3e7c6cff --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'ReplyNotificationSettings' +> +export function ReplyNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={BubbleIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Replies</Trans>} + subtitleText={ + <Trans> + Get notifications when people reply to your posts. + </Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="reply" + preference={preferences?.reply} + allowDisableInApp={false} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx new file mode 100644 index 000000000..aa9e4e32f --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx @@ -0,0 +1,63 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostNotificationSettings' +> +export function RepostNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={RepostIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Reposts</Trans>} + subtitleText={ + <Trans>Get notifications when people repost your posts.</Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="repost" + preference={preferences?.repost} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx new file mode 100644 index 000000000..13fec6168 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx @@ -0,0 +1,66 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon} from '#/components/icons/Repost' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' +import {PreferenceControls} from './components/PreferenceControls' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'RepostsOnRepostsNotificationSettings' +> +export function RepostsOnRepostsNotificationSettingsScreen({}: Props) { + const {data: preferences, isError} = useNotificationSettingsQuery() + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + <SettingsList.Item style={[a.align_start]}> + <SettingsList.ItemIcon icon={RepostRepostIcon} /> + <ItemTextWithSubtitle + bold + titleText={<Trans>Reposts of your reposts</Trans>} + subtitleText={ + <Trans> + Get notifications when people repost posts that you've + reposted. + </Trans> + } + /> + </SettingsList.Item> + {isError ? ( + <View style={[a.px_lg, a.pt_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + ) : ( + <PreferenceControls + name="repostViaRepost" + preference={preferences?.repostViaRepost} + /> + )} + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx new file mode 100644 index 000000000..217fc33b9 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx @@ -0,0 +1,34 @@ +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import * as Skele from '#/components/Skeleton' +import {Text} from '#/components/Typography' +import * as SettingsList from '../../components/SettingsList' + +export function ItemTextWithSubtitle({ + titleText, + subtitleText, + bold = false, + showSkeleton = false, +}: { + titleText: React.ReactNode + subtitleText: React.ReactNode + bold?: boolean + showSkeleton?: boolean +}) { + const t = useTheme() + return ( + <View style={[a.flex_1, bold ? a.gap_xs : a.gap_2xs]}> + <SettingsList.ItemText style={bold && [a.font_bold, a.text_lg]}> + {titleText} + </SettingsList.ItemText> + {showSkeleton ? ( + <Skele.Text style={[a.text_sm, {width: 120}]} /> + ) : ( + <Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}> + {subtitleText} + </Text> + )} + </View> + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx new file mode 100644 index 000000000..336e08695 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -0,0 +1,194 @@ +import {useMemo} from 'react' +import {View} from 'react-native' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky/notification/defs' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings' +import {atoms as a, platform, useTheme} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' +import {Divider} from '../../components/SettingsList' + +export function PreferenceControls({ + name, + syncOthers, + preference, + allowDisableInApp = true, +}: { + name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'> + /** + * Keep other prefs in sync with `name`. For use in the "everything else" category + * which groups starterpack joins + verified + unverified notifications into a single toggle. + */ + syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[] + preference?: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp?: boolean +}) { + if (!preference) + return ( + <View style={[a.w_full, a.pt_5xl, a.align_center]}> + <Loader size="xl" /> + </View> + ) + + return ( + <Inner + name={name} + syncOthers={syncOthers} + preference={preference} + allowDisableInApp={allowDisableInApp} + /> + ) +} + +export function Inner({ + name, + syncOthers = [], + preference, + allowDisableInApp, +}: { + name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'> + syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[] + preference: AppBskyNotificationDefs.Preference | FilterablePreference + allowDisableInApp: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {mutate} = useNotificationSettingsUpdateMutation() + + const channels = useMemo(() => { + const arr = [] + if (preference.list) arr.push('list') + if (preference.push) arr.push('push') + return arr + }, [preference]) + + const onChangeChannels = (change: string[]) => { + const newPreference = { + ...preference, + list: change.includes('list'), + push: change.includes('push'), + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + const onChangeFilter = ([change]: string[]) => { + if (change !== 'all' && change !== 'follows') + throw new Error('Invalid filter') + + const newPreference = { + ...preference, + filter: change, + } satisfies typeof preference + + mutate({ + [name]: newPreference, + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), + }) + } + + return ( + <View style={[a.px_xl, a.pt_md, a.gap_sm]}> + <Toggle.Group + type="checkbox" + label={_(`Select your preferred notification channels`)} + values={channels} + onChange={onChangeChannels}> + <View style={[a.gap_sm]}> + <Toggle.Item + label={_(msg`Receive push notifications`)} + name="push" + style={[ + a.py_xs, + platform({ + native: [a.justify_between], + web: [a.flex_row_reverse, a.gap_md], + }), + ]}> + <Toggle.LabelText + style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> + <Trans>Push notifications</Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + {allowDisableInApp && ( + <Toggle.Item + label={_(msg`Receive in-app notifications`)} + name="list" + style={[ + a.py_xs, + platform({ + native: [a.justify_between], + web: [a.flex_row_reverse, a.gap_md], + }), + ]}> + <Toggle.LabelText + style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> + <Trans>In-app notifications</Trans> + </Toggle.LabelText> + <Toggle.Platform /> + </Toggle.Item> + )} + </View> + </Toggle.Group> + {'filter' in preference && ( + <> + <Divider /> + <Text style={[a.font_bold, a.text_md]}>From</Text> + <Toggle.Group + type="radio" + label={_('Filter who you receive notifications from')} + values={[preference.filter]} + onChange={onChangeFilter} + disabled={channels.length === 0}> + <View style={[a.gap_sm]}> + <Toggle.Item + label={_(msg`Everyone`)} + name="all" + style={[ + a.flex_row, + a.py_xs, + platform({native: [a.gap_sm], web: [a.gap_md]}), + ]}> + <Toggle.Radio /> + <Toggle.LabelText + style={[ + channels.length > 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + <Trans>Everyone</Trans> + </Toggle.LabelText> + </Toggle.Item> + <Toggle.Item + label={_(msg`People I follow`)} + name="follows" + style={[ + a.flex_row, + a.py_xs, + platform({native: [a.gap_sm], web: [a.gap_md]}), + ]}> + <Toggle.Radio /> + <Toggle.LabelText + style={[ + channels.length > 0 && t.atoms.text, + a.font_normal, + a.text_md, + ]}> + <Trans>People I follow</Trans> + </Toggle.LabelText> + </Toggle.Item> + </View> + </Toggle.Group> + </> + )} + </View> + ) +} diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx new file mode 100644 index 000000000..a4f6dede0 --- /dev/null +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -0,0 +1,293 @@ +import {useEffect} from 'react' +import {Linking, View} from 'react-native' +import * as Notification from 'expo-notifications' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useQuery, useQueryClient} from '@tanstack/react-query' + +import {useAppState} from '#/lib/hooks/useAppState' +import { + type AllNavigatorParams, + type NativeStackScreenProps, +} from '#/lib/routes/types' +import {isAndroid, isIOS, isWeb} from '#/platform/detection' +import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +// import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' +import { + Heart2_Stroke2_Corner0_Rounded as HeartIcon, + LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon, +} from '#/components/icons/Heart2' +import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' +import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' +import { + Repost_Stroke2_Corner2_Rounded as RepostIcon, + RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, +} from '#/components/icons/Repost' +import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' +import * as Layout from '#/components/Layout' +import * as SettingsList from '../components/SettingsList' +import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' + +const RQKEY = ['notification-permissions'] + +type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'> +export function NotificationSettingsScreen({}: Props) { + const {_} = useLingui() + const queryClient = useQueryClient() + const {data: settings, isError} = useNotificationSettingsQuery() + + const {data: permissions, refetch} = useQuery({ + queryKey: RQKEY, + queryFn: async () => { + if (isWeb) return null + return await Notification.getPermissionsAsync() + }, + }) + + const appState = useAppState() + useEffect(() => { + if (appState === 'active') { + refetch() + } + }, [appState, refetch]) + + const onRequestPermissions = async () => { + if (isWeb) return + if (permissions?.canAskAgain) { + const response = await Notification.requestPermissionsAsync() + queryClient.setQueryData(RQKEY, response) + } else { + if (isAndroid) { + try { + await Linking.sendIntent( + 'android.settings.APP_NOTIFICATION_SETTINGS', + [ + { + key: 'android.provider.extra.APP_PACKAGE', + value: 'xyz.blueskyweb.app', + }, + ], + ) + } catch { + Linking.openSettings() + } + } else if (isIOS) { + Linking.openSettings() + } + } + } + + return ( + <Layout.Screen> + <Layout.Header.Outer> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot /> + </Layout.Header.Outer> + <Layout.Content> + <SettingsList.Container> + {permissions && !permissions.granted && ( + <> + <SettingsList.PressableItem + label={_(msg`Enable push notifications`)} + onPress={onRequestPermissions}> + <SettingsList.ItemIcon icon={HapticIcon} /> + <SettingsList.ItemText> + <Trans>Enable push notifications</Trans> + </SettingsList.ItemText> + </SettingsList.PressableItem> + <SettingsList.Divider /> + </> + )} + {isError && ( + <View style={[a.px_lg, a.pb_md]}> + <Admonition type="error"> + <Trans>Failed to load notification settings.</Trans> + </Admonition> + </View> + )} + <View style={[a.gap_sm]}> + <SettingsList.LinkItem + label={_(msg`Settings for reply notifications`)} + to={{screen: 'ReplyNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={BubbleIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Replies</Trans>} + subtitleText={<SettingPreview preference={settings?.reply} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for mention notifications`)} + to={{screen: 'MentionNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={AtIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Mentions</Trans>} + subtitleText={<SettingPreview preference={settings?.mention} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for quote notifications`)} + to={{screen: 'QuoteNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={CloseQuoteIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Quotes</Trans>} + subtitleText={<SettingPreview preference={settings?.quote} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for like notifications`)} + to={{screen: 'LikeNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={HeartIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Likes</Trans>} + subtitleText={<SettingPreview preference={settings?.like} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for repost notifications`)} + to={{screen: 'RepostNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={RepostIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Reposts</Trans>} + subtitleText={<SettingPreview preference={settings?.repost} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for new follower notifications`)} + to={{screen: 'NewFollowerNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={PersonPlusIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>New followers</Trans>} + subtitleText={<SettingPreview preference={settings?.follow} />} + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + {/* <SettingsList.LinkItem + label={_(msg`Settings for activity alerts`)} + to={{screen: 'ActivityNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={BellRingingIcon} /> + + <ItemTextWithSubtitle + titleText={<Trans>Activity alerts</Trans>} + subtitleText={ + <SettingPreview preference={settings?.subscribedPost} /> + } + showSkeleton={!settings} + /> + </SettingsList.LinkItem> */} + <SettingsList.LinkItem + label={_( + msg`Settings for notifications for likes on your reposts`, + )} + to={{screen: 'LikesOnRepostsNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={LikeRepostIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Likes on your reposts</Trans>} + subtitleText={ + <SettingPreview preference={settings?.likeViaRepost} /> + } + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_( + msg`Settings for notifications for reposts of your reposts`, + )} + to={{screen: 'RepostsOnRepostsNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={RepostRepostIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Reposts of your reposts</Trans>} + subtitleText={ + <SettingPreview preference={settings?.repostViaRepost} /> + } + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + <SettingsList.LinkItem + label={_(msg`Settings for notifications for everything else`)} + to={{screen: 'MiscellaneousNotificationSettings'}} + contentContainerStyle={[a.align_start]}> + <SettingsList.ItemIcon icon={ShapesIcon} /> + <ItemTextWithSubtitle + titleText={<Trans>Everything else</Trans>} + // technically a bundle of several settings, but since they're set together + // and are most likely in sync we'll just show the state of one of them + subtitleText={ + <SettingPreview preference={settings?.starterpackJoined} /> + } + showSkeleton={!settings} + /> + </SettingsList.LinkItem> + </View> + </SettingsList.Container> + </Layout.Content> + </Layout.Screen> + ) +} + +function SettingPreview({ + preference, +}: { + preference?: + | AppBskyNotificationDefs.Preference + | AppBskyNotificationDefs.FilterablePreference +}) { + const {_} = useLingui() + if (!preference) { + return null + } else { + if ('filter' in preference) { + if (preference.filter === 'all') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, Everyone`) + } else if (preference.list) { + return _(msg`In-app, Everyone`) + } else if (preference.push) { + return _(msg`Push, Everyone`) + } + } else if (preference.filter === 'follows') { + if (preference.list && preference.push) { + return _(msg`In-app, Push, People you follow`) + } else if (preference.list) { + return _(msg`In-app, People you follow`) + } else if (preference.push) { + return _(msg`Push, People you follow`) + } + } + } else { + if (preference.list && preference.push) { + return _(msg`In-app, Push`) + } else if (preference.list) { + return _(msg`In-app`) + } else if (preference.push) { + return _(msg`Push`) + } + } + } + + return _(msg`Off`) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 9f36c27ac..6310c7c3c 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -36,6 +36,7 @@ import {AvatarStackWithFetch} from '#/components/AvatarStack' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' +import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' @@ -181,6 +182,14 @@ export function SettingsScreen({}: Props) { </SettingsList.ItemText> </SettingsList.LinkItem> <SettingsList.LinkItem + to="/settings/notifications" + label={_(msg`Notifications`)}> + <SettingsList.ItemIcon icon={NotificationIcon} /> + <SettingsList.ItemText> + <Trans>Notifications</Trans> + </SettingsList.ItemText> + </SettingsList.LinkItem> + <SettingsList.LinkItem to="/settings/content-and-media" label={_(msg`Content and media`)}> <SettingsList.ItemIcon icon={WindowIcon} /> diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index 2ac42aa32..9661bed1b 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -1,72 +1,63 @@ -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useMutation, useQueryClient} from '@tanstack/react-query' +import {type AppBskyNotificationDefs} from '@atproto/api' +import {t} from '@lingui/macro' +import { + type QueryClient, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' -import {until} from '#/lib/async/until' import {logger} from '#/logger' -import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' -import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' import {useAgent} from '#/state/session' import * as Toast from '#/view/com/util/Toast' -export function useNotificationSettingsMutation() { - const {_} = useLingui() +const RQKEY_ROOT = 'notification-settings' +const RQKEY = [RQKEY_ROOT] + +export function useNotificationSettingsQuery() { + const agent = useAgent() + + return useQuery({ + queryKey: RQKEY, + queryFn: async () => { + const response = await agent.app.bsky.notification.getPreferences() + return response.data.preferences + }, + }) +} +export function useNotificationSettingsUpdateMutation() { const agent = useAgent() const queryClient = useQueryClient() return useMutation({ - mutationFn: async (keys: string[]) => { - const enabled = keys[0] === 'enabled' - - await agent.api.app.bsky.notification.putPreferences({ - priority: enabled, - }) - - await until( - 5, // 5 tries - 1e3, // 1s delay between tries - res => res.data.priority === enabled, - () => agent.api.app.bsky.notification.listNotifications({limit: 1}), - ) - - eagerlySetCachedPriority(queryClient, enabled) - }, - onError: err => { - logger.error('Failed to save notification preferences', { - safeMessage: err, - }) - Toast.show( - _(msg`Failed to save notification preferences, please try again`), - 'xmark', + mutationFn: async ( + update: Partial<AppBskyNotificationDefs.Preferences>, + ) => { + const response = await agent.app.bsky.notification.putPreferencesV2( + update, ) + return response.data.preferences }, - onSuccess: () => { - Toast.show(_(msg({message: 'Preference saved', context: 'toast'}))) + onMutate: update => { + optimisticUpdateNotificationSettings(queryClient, update) }, - onSettled: () => { - invalidateCachedUnreadPage() - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) + onError: e => { + logger.error('Could not update notification settings', {message: e}) + queryClient.invalidateQueries({queryKey: RQKEY}) + Toast.show(t`Could not update notification settings`, 'xmark') }, }) } -function eagerlySetCachedPriority( - queryClient: ReturnType<typeof useQueryClient>, - enabled: boolean, +function optimisticUpdateNotificationSettings( + queryClient: QueryClient, + update: Partial<AppBskyNotificationDefs.Preferences>, ) { - function updateData(old: any) { - if (!old) return old - return { - ...old, - pages: old.pages.map((page: any) => { - return { - ...page, - priority: enabled, - } - }), - } - } - queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) - queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) + queryClient.setQueryData( + RQKEY, + (old?: AppBskyNotificationDefs.Preferences) => { + if (!old) return old + return {...old, ...update} + }, + ) } diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index ace0de2ae..528d6be87 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -130,7 +130,7 @@ export function NotificationsScreen({}: Props) { </Layout.Header.Content> <Layout.Header.Slot> <Link - to="/notifications/settings" + to={{screen: 'NotificationSettings'}} label={_(msg`Notification settings`)} size="small" variant="ghost" diff --git a/yarn.lock b/yarn.lock index fb126ae73..a63dbc9b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,13 +55,13 @@ resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz#f39098747dabf8a245d0ed6edc50f362aa4d95f8" integrity sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA== -"@atproto-labs/xrpc-utils@0.0.14": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.14.tgz#eefe1ccf61a4288708601324496b0106d5ed4ae3" - integrity sha512-/f0Dhzi08w3Oqv38wdwQ5bw238GbxhYIcxg08kVReEMTlkyRDC6H5RuqHf8Ff9J3FKqjKHGdxaOdrPNM1hCgeQ== +"@atproto-labs/xrpc-utils@0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.16.tgz#f76c4f615685c60997401f052cbd9f0145d12576" + integrity sha512-WvTQhGjIhFrd/0pMGecE7Xn8BtvvKAgVlNs8UaE6CVRifiCOIvIBwlx1vnslJAavK3FtwL1kKkUdxNtxHciZSQ== dependencies: "@atproto/xrpc" "^0.7.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" "@atproto/api@^0.15.15": version "0.15.15" @@ -77,20 +77,6 @@ tlds "^1.234.0" zod "^3.23.8" -"@atproto/api@^0.15.9": - version "0.15.9" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.9.tgz#f8c40afd6e414ab107d63d6f08d9e264bf9a149a" - integrity sha512-CyAILiIcbN+V5CFAI6MDb247epm25RGkP7HSan5LUaOHiyg1NCAmflWCN/bbMdJX9kLqjAPAG3eN4BUUbYe//Q== - dependencies: - "@atproto/common-web" "^0.4.2" - "@atproto/lexicon" "^0.4.11" - "@atproto/syntax" "^0.4.0" - "@atproto/xrpc" "^0.7.0" - await-lock "^2.2.2" - multiformats "^9.9.0" - tlds "^1.234.0" - zod "^3.23.8" - "@atproto/aws@^0.2.21": version "0.2.21" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.21.tgz#06006a101c8004db11384a19366296cd87468326" @@ -108,23 +94,23 @@ multiformats "^9.9.0" uint8arrays "3.0.0" -"@atproto/bsky@^0.0.151": - version "0.0.151" - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.151.tgz#a0e5b59e163a3b74379fb547601be4fc66b7a133" - integrity sha512-42pvUsyGw0nR6Sxlda824maY4gBxUni1cXPG+7uGe6Ixm6XAaPhfTgT1rAg++1rDXH9tT1EXAVnMxg38S6osLg== +"@atproto/bsky@^0.0.159": + version "0.0.159" + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.159.tgz#6fcfd7d6c73e4041c5abc9ac3b99dbe0e1e6cf76" + integrity sha512-kRjDCW6FbByeafrEoUD5YMhhjuKTvSqrE2/QJ5xe9CP8UIhl8BShm2PcBh9gJtYc7lO83aJPqDSqb5gJwNAJUg== dependencies: "@atproto-labs/fetch-node" "0.1.9" - "@atproto-labs/xrpc-utils" "0.0.14" - "@atproto/api" "^0.15.9" + "@atproto-labs/xrpc-utils" "0.0.16" + "@atproto/api" "^0.15.15" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/did" "^0.1.5" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" "@atproto/repo" "^0.8.1" - "@atproto/sync" "^0.1.23" + "@atproto/sync" "^0.1.25" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" "@bufbuild/protobuf" "^1.5.0" "@connectrpc/connect" "^1.1.4" "@connectrpc/connect-express" "^1.1.4" @@ -154,10 +140,10 @@ uint8arrays "3.0.0" undici "^6.19.8" -"@atproto/bsync@^0.0.19": - version "0.0.19" - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.19.tgz#bab3d5e4e7c1ca8de16d9b5efebc49dde12d7160" - integrity sha512-AF9aWbU0VlpT//lIuYKhNRplTv+99ld58kfHTS8jfXCpiOZwxwneTkB1hzE+slXJ63K8i/GyzsQCyvRHWzGWCQ== +"@atproto/bsync@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.20.tgz#7409c39a1d4715a5be4ce4a545155de238fc9469" + integrity sha512-KbHoZcFpKY869dMQRZXrOXccMkndgLiDY4sfLkCTgp/A0pWw3CKuJmQSmtKHIkWcVkOnfsfJW0J/SgyrXLrn9Q== dependencies: "@atproto/common" "^0.4.11" "@atproto/syntax" "^0.4.0" @@ -232,23 +218,23 @@ "@noble/hashes" "^1.6.1" uint8arrays "3.0.0" -"@atproto/dev-env@^0.3.133": - version "0.3.133" - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.133.tgz#4ca58c9c4c99f001f26ce50629214f81d6acd3ab" - integrity sha512-GtKDa+q0Fx2tJZL44cDAINMCxNmt1aKkGVpW/6PTnuSSjdA7ErBUEL3opbwgaAcPRGZfscB0mQmGfWR0BUmvUw== +"@atproto/dev-env@^0.3.142": + version "0.3.142" + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.142.tgz#466cff00c92d53ad148709ae50bcca816fcee1bc" + integrity sha512-NiNb3Pdj93goEmKBIF5rIlLSmkfZiwnYmo7U6cGvGXoopRNG5e4Vm2PXb1n7uVdzuvEtiPtpswQxaf0x6jJsWA== dependencies: - "@atproto/api" "^0.15.9" - "@atproto/bsky" "^0.0.151" - "@atproto/bsync" "^0.0.19" + "@atproto/api" "^0.15.15" + "@atproto/bsky" "^0.0.159" + "@atproto/bsync" "^0.0.20" "@atproto/common-web" "^0.4.2" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" - "@atproto/ozone" "^0.1.112" - "@atproto/pds" "^0.4.139" - "@atproto/sync" "^0.1.23" + "@atproto/ozone" "^0.1.120" + "@atproto/pds" "^0.4.148" + "@atproto/sync" "^0.1.25" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" "@did-plc/lib" "^0.0.1" "@did-plc/server" "^0.0.1" dotenv "^16.0.3" @@ -258,7 +244,7 @@ uint8arrays "3.0.0" undici "^6.14.1" -"@atproto/did@^0.1.5": +"@atproto/did@0.1.5", "@atproto/did@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.1.5.tgz#5bfe73625d54c4c687c00ff370971ce01c39bd61" integrity sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ== @@ -273,18 +259,18 @@ "@atproto/common-web" "^0.4.2" "@atproto/crypto" "^0.4.4" -"@atproto/jwk-jose@0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.6.tgz#e9fc5a714cd9fe0589f931f3b406585716b5ac03" - integrity sha512-r4DGMvvmazy6CxqAcnplpUxvp6Vd8UwKxQBZRpmm1aNsVonf5qj1yeDkECTiwoe/FPbvtdamlzClB3UZc7Yb5w== +"@atproto/jwk-jose@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.8.tgz#2dc8ad2cc900e7bc231add293f6518b06dc017ec" + integrity sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA== dependencies: - "@atproto/jwk" "0.1.5" + "@atproto/jwk" "0.3.0" jose "^5.2.0" -"@atproto/jwk@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.1.5.tgz#d1e2650431f7f09ed80be48f05908bcd136c2606" - integrity sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg== +"@atproto/jwk@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.3.0.tgz#275fa676f6b5988ddedf4ee0475dd285de9b831b" + integrity sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ== dependencies: multiformats "^9.9.0" zod "^3.23.8" @@ -300,32 +286,32 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/oauth-provider-api@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.2.tgz#cdca03af4426f8cf9b09bc9eb57a8604f9513831" - integrity sha512-tNAuMrE6D3696euavxo1+Jh7Re0PPwJstbyY8SrdVPXgKJh/LrbpKUKiPNW/p5KyVfRs2tWeAxy+ReESu6SmXA== +"@atproto/oauth-provider-api@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.4.tgz#a775182e3648dc693a04e3cb604eb62cd9ddfd8c" + integrity sha512-3PRrf0gTAVMCETjtIH/3AaQaHBDbjsRBc/OYrlWBZ9IPplchBXtQGH/KcnjE4kK2Ef8p45qQSl3dNWg3EXsbHQ== dependencies: - "@atproto/jwk" "0.1.5" - "@atproto/oauth-types" "0.2.7" + "@atproto/jwk" "0.3.0" + "@atproto/oauth-types" "0.3.0" -"@atproto/oauth-provider-frontend@0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.5.tgz#66fd8760fade2ac94111ad5389f33f4d8ce5bba2" - integrity sha512-FdDBuwy827+etjIcRwZU7dtxa8Ltso3ufVLMEi8A2V91v21XDysZjLANC6cvmNNSUcS4E/J6ZAwTrQDo7O5axw== +"@atproto/oauth-provider-frontend@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.8.tgz#21d944566c63f54524f239a10f7c65d150982f40" + integrity sha512-uqfHv+n2xq7vTpuBP1Red7PhpaAbbJbwSbRsSfplJQ16XmF5NCMU8dHGCGRTEHngLZ9UquuIefN3w1QTrNzD0w== optionalDependencies: - "@atproto/oauth-provider-api" "0.1.2" + "@atproto/oauth-provider-api" "0.1.4" -"@atproto/oauth-provider-ui@0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.6.tgz#4bae995ff57671ac3915f58fdb2cf6a76a0fe42d" - integrity sha512-pJzV9ouNj1/TDUCl3CWEZrHoUese4lcKx5F59t2OiLFm2K7T7QrszKUIMyU5QdiQHv551B0ZJOkJ8+4b/fVGPA== +"@atproto/oauth-provider-ui@0.1.9": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.9.tgz#8c43a1affa94ecb537072e6d569b8a24cdd42e72" + integrity sha512-a6/VAeQWRMxpgnqo/TuqXg3EW2tO68jLh8Mv1uyV1NiZbT7fNlgkII/djIl3fLoEa95I3p236NZxjhKELSBbGg== optionalDependencies: - "@atproto/oauth-provider-api" "0.1.2" + "@atproto/oauth-provider-api" "0.1.4" -"@atproto/oauth-provider@^0.7.8": - version "0.7.8" - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.8.tgz#287b15eb6b0bc0bb4b2da2339150253db006c6e0" - integrity sha512-+dEU9dTyfWKeZ/Nu7ocR6fO73RcG0vwDjT45vgcnM9L7jtuPk9zfpmiR4ODYBk9QUu2DURo9yBhtXNJI3Yz8aQ== +"@atproto/oauth-provider@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.9.0.tgz#3598924978c2e3d5fdf62bced54574156d15cf92" + integrity sha512-LbZS9rbR5l9gVO97wJ3ls+ENXwv6BakmArRyjc5EfaQ4Xc3eLbvE629hpu9LV8LyCkBpseum0l+D+rYXsemNUw== dependencies: "@atproto-labs/fetch" "0.2.3" "@atproto-labs/fetch-node" "0.1.9" @@ -333,12 +319,13 @@ "@atproto-labs/simple-store" "0.2.0" "@atproto-labs/simple-store-memory" "0.1.3" "@atproto/common" "^0.4.11" - "@atproto/jwk" "0.1.5" - "@atproto/jwk-jose" "0.1.6" - "@atproto/oauth-provider-api" "0.1.2" - "@atproto/oauth-provider-frontend" "0.1.5" - "@atproto/oauth-provider-ui" "0.1.6" - "@atproto/oauth-types" "0.2.7" + "@atproto/did" "0.1.5" + "@atproto/jwk" "0.3.0" + "@atproto/jwk-jose" "0.1.8" + "@atproto/oauth-provider-api" "0.1.4" + "@atproto/oauth-provider-frontend" "0.1.8" + "@atproto/oauth-provider-ui" "0.1.9" + "@atproto/oauth-types" "0.3.0" "@atproto/syntax" "0.4.0" "@hapi/accept" "^6.0.3" "@hapi/address" "^5.1.1" @@ -352,27 +339,27 @@ jose "^5.2.0" zod "^3.23.8" -"@atproto/oauth-types@0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.7.tgz#c210868052f8babd98510c19816e3d9a156b33c7" - integrity sha512-2SlDveiSI0oowC+sfuNd/npV8jw/FhokSS26qyUyldTg1g9ZlhxXUfMP4IZOPeZcVn9EszzQRHs1H9ZJqVQIew== +"@atproto/oauth-types@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.3.0.tgz#8d49d939486ac281bc13d0b1fe4462b7e519fdf0" + integrity sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA== dependencies: - "@atproto/jwk" "0.1.5" + "@atproto/jwk" "0.3.0" zod "^3.23.8" -"@atproto/ozone@^0.1.112": - version "0.1.112" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.112.tgz#6b6b5ac052dd4e6dfec3c88f83c9b53f4902fcbe" - integrity sha512-Euut64N/4UyRXyV6m1ATE9K6o6EpCf46ozD4GG8HJ9AC5zEgBYMSkH4l6SLrhKrYYIGXkvglk1WYuuDQKYb3LA== +"@atproto/ozone@^0.1.120": + version "0.1.120" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.120.tgz#b5c431a538558179de0465cfbfea512d65e092bf" + integrity sha512-zu2f16K/z/3r4mC4z/8qISPt0j+Y0GwtjmSE+VOJvVT363iOd9a834K+QHJqnD6B3iTBHR1VPlZ/4fsZ3+4UaA== dependencies: - "@atproto/api" "^0.15.9" + "@atproto/api" "^0.15.15" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" "@atproto/syntax" "^0.4.0" "@atproto/xrpc" "^0.7.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" "@did-plc/lib" "^0.0.1" compression "^1.7.4" cors "^2.8.5" @@ -390,24 +377,24 @@ undici "^6.14.1" ws "^8.12.0" -"@atproto/pds@^0.4.139": - version "0.4.139" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.139.tgz#70ae5afd7d90eab214c652d57a5e6478af454fbe" - integrity sha512-VD1VTSAnbAme4D4Xk/Wdl05qs8YbCe39/i960EyXzw2fYNvL9jMpKm3z0lwhrYN9q7phFhr2ubU2QjfRFDbDAQ== +"@atproto/pds@^0.4.148": + version "0.4.148" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.148.tgz#7af82480e42174ea1c284f5975bf0aff3bd4da24" + integrity sha512-PbxTpxRAcsdu3zANjwNH+Pfbu0pfj5z6UDmcnc3eaEP11xsExu3+B84jOYKAkIN/PbM1A9EbiBjKb95yBQxGAw== dependencies: "@atproto-labs/fetch-node" "0.1.9" - "@atproto-labs/xrpc-utils" "0.0.14" - "@atproto/api" "^0.15.9" + "@atproto-labs/xrpc-utils" "0.0.16" + "@atproto/api" "^0.15.15" "@atproto/aws" "^0.2.21" "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" - "@atproto/oauth-provider" "^0.7.8" + "@atproto/oauth-provider" "^0.9.0" "@atproto/repo" "^0.8.1" "@atproto/syntax" "^0.4.0" "@atproto/xrpc" "^0.7.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" "@did-plc/lib" "^0.0.4" "@hapi/address" "^5.1.1" better-sqlite3 "^10.0.0" @@ -452,17 +439,17 @@ varint "^6.0.0" zod "^3.23.8" -"@atproto/sync@^0.1.23": - version "0.1.23" - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.23.tgz#01d4ecf9d5ddc624d14e8fb98927f0b2a97eafeb" - integrity sha512-1ItRNHMLMcBeTziOZpxS4Q+ha2enQce3fSiAQaCpLCQ8VTNq1D1aRR6ePZCQFzab9jDDtBz0v4FufOnMByRIeg== +"@atproto/sync@^0.1.25": + version "0.1.25" + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.25.tgz#66b3453e3cf0ba6a155dbbf207ea46d632c2d6b0" + integrity sha512-4UsQgQsUK+hKFAEDi10Ops6n2W/kfk2JYP8AU6FSHAzOadB1hKRDJbGF5vLiLP9ACBhCzoJerZ31DCnhjzRzfw== dependencies: "@atproto/common" "^0.4.11" "@atproto/identity" "^0.4.8" "@atproto/lexicon" "^0.4.11" "@atproto/repo" "^0.8.1" "@atproto/syntax" "^0.4.0" - "@atproto/xrpc-server" "^0.7.18" + "@atproto/xrpc-server" "^0.8.0" multiformats "^9.9.0" p-queue "^6.6.2" ws "^8.12.0" @@ -472,10 +459,10 @@ resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2" integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA== -"@atproto/xrpc-server@^0.7.18": - version "0.7.18" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.18.tgz#7cb6e517da2afec1c9bee70d92c07667a80718ec" - integrity sha512-kjlAsI+UNbbm6AK3Y5Hb4BJ7VQHNKiYYu2kX5vhZJZHO8qfO40GPYYb/2TknZV8IG6fDPBQhUpcDRolI86sgag== +"@atproto/xrpc-server@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.8.0.tgz#a32c9c71411ec6ee476fcd0260d5e9e80be348bd" + integrity sha512-jDAEVHVhM4IvC0y491gXBuD4b1D9/XrM3HaEronRneAdNZ0qE0nsiJNqiHfQ6r4BvFdHnABM9KyHV9EQTvmxfg== dependencies: "@atproto/common" "^0.4.11" "@atproto/crypto" "^0.4.4" |