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>
This commit is contained in:
Samuel Newman
2025-06-17 12:37:14 +03:00
committed by GitHub
parent 7dc6bb57a6
commit 21989b558b
34 changed files with 1434 additions and 287 deletions
@@ -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>

After

Width:  |  Height:  |  Size: 809 B

@@ -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>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -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>

After

Width:  |  Height:  |  Size: 979 B

@@ -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>

After

Width:  |  Height:  |  Size: 823 B

+11
View File
@@ -278,6 +278,17 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/content-and-media", server.WebGeneric) e.GET("/settings/content-and-media", server.WebGeneric)
e.GET("/settings/interests", server.WebGeneric) e.GET("/settings/interests", server.WebGeneric)
e.GET("/settings/about", 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("/settings/app-icon", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric) e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/debug-mod", server.WebGeneric) e.GET("/sys/debug-mod", server.WebGeneric)
+1 -1
View File
@@ -218,7 +218,7 @@
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@atproto/dev-env": "^0.3.133", "@atproto/dev-env": "^0.3.142",
"@babel/core": "^7.26.0", "@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0", "@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0", "@babel/runtime": "^7.26.0",
+93 -6
View File
@@ -90,11 +90,10 @@ import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from '#/screens/Settings/Settings' import {SettingsScreen} from '#/screens/Settings/Settings'
import {SettingsInterests} from '#/screens/Settings/SettingsInterests'
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
import { import {
StarterPackScreen, StarterPackScreen,
@@ -110,6 +109,17 @@ import {
} from '#/components/dialogs/EmailDialog' } from '#/components/dialogs/EmailDialog'
import {router} from '#/routes' import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army' 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>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -380,6 +390,83 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
requireAuth: true, requireAuth: true,
}} }}
/> />
<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 <Stack.Screen
name="ContentAndMediaSettings" name="ContentAndMediaSettings"
getComponent={() => ContentAndMediaSettingsScreen} getComponent={() => ContentAndMediaSettingsScreen}
@@ -389,8 +476,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
}} }}
/> />
<Stack.Screen <Stack.Screen
name="SettingsInterests" name="InterestsSettings"
getComponent={() => SettingsInterests} getComponent={() => InterestsSettingsScreen}
options={{ options={{
title: title(msg`Your interests`), title: title(msg`Your interests`),
requireAuth: true, requireAuth: true,
@@ -438,8 +525,8 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
options={{title: title(msg`Chat request inbox`), requireAuth: true}} options={{title: title(msg`Chat request inbox`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="NotificationSettings" name="LegacyNotificationSettings"
getComponent={() => NotificationSettingsScreen} getComponent={() => LegacyNotificationSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}} options={{title: title(msg`Notification settings`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
+5
View File
@@ -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',
})
+4
View File
@@ -7,3 +7,7 @@ export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({
export const Heart2_Filled_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', 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',
})
+4
View File
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Phone_Stroke2_Corner0_Rounded = createSinglePathSVG({ 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', 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',
})
+4
View File
@@ -11,3 +11,7 @@ export const Repost_Stroke2_Corner2_Rounded = createSinglePathSVG({
export const Repost_Stroke2_Corner3_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', 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',
})
+3 -3
View File
@@ -1,8 +1,8 @@
import {type Route, type RouteParams} from './types' import {type Route, type RouteParams} from './types'
export class Router { export class Router<T extends Record<string, any>> {
routes: [string, Route][] = [] routes: [string, Route][] = []
constructor(description: Record<string, string | string[]>) { constructor(description: Record<keyof T, string | string[]>) {
for (const [screen, pattern] of Object.entries(description)) { for (const [screen, pattern] of Object.entries(description)) {
if (typeof pattern === 'string') { if (typeof pattern === 'string') {
this.routes.push([screen, createRoute(pattern)]) 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) { for (const [screenName, route] of this.routes) {
if (screenName === name) { if (screenName === name) {
return route return route
+13 -11
View File
@@ -52,7 +52,18 @@ export type CommonNavigatorParams = {
AccountSettings: undefined AccountSettings: undefined
PrivacyAndSecuritySettings: undefined PrivacyAndSecuritySettings: undefined
ContentAndMediaSettings: 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 AboutSettings: undefined
AppIconSettings: undefined AppIconSettings: undefined
Search: {q?: string} Search: {q?: string}
@@ -61,7 +72,7 @@ export type CommonNavigatorParams = {
MessagesConversation: {conversation: string; embed?: string; accept?: true} MessagesConversation: {conversation: string; embed?: string; accept?: true}
MessagesSettings: undefined MessagesSettings: undefined
MessagesInbox: undefined MessagesInbox: undefined
NotificationSettings: undefined LegacyNotificationSettings: undefined
Feeds: undefined Feeds: undefined
Start: {name: string; rkey: string} Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
@@ -104,8 +115,6 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
Notifications: undefined Notifications: undefined
Hashtag: {tag: string; author?: string}
Topic: {topic: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
} }
@@ -118,15 +127,8 @@ export type AllNavigatorParams = CommonNavigatorParams & {
NotificationsTab: undefined NotificationsTab: undefined
Notifications: undefined Notifications: undefined
MyProfileTab: undefined MyProfileTab: undefined
Hashtag: {tag: string; author?: string}
Topic: {topic: string}
MessagesTab: undefined MessagesTab: undefined
Messages: {animation?: 'push' | 'pop'} 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 // NOTE
+22 -4
View File
@@ -1,11 +1,17 @@
import {Router} from '#/lib/routes/router' 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: '/', Home: '/',
Search: '/search', Search: '/search',
Feeds: '/feeds', Feeds: '/feeds',
Notifications: '/notifications', Notifications: '/notifications',
NotificationSettings: '/notifications/settings', LegacyNotificationSettings: '/notifications/settings',
Settings: '/settings', Settings: '/settings',
Lists: '/lists', Lists: '/lists',
// moderation // moderation
@@ -42,13 +48,25 @@ export const router = new Router({
AccessibilitySettings: '/settings/accessibility', AccessibilitySettings: '/settings/accessibility',
AppearanceSettings: '/settings/appearance', AppearanceSettings: '/settings/appearance',
SavedFeeds: '/settings/saved-feeds', SavedFeeds: '/settings/saved-feeds',
// new settings
AccountSettings: '/settings/account', AccountSettings: '/settings/account',
PrivacyAndSecuritySettings: '/settings/privacy-and-security', PrivacyAndSecuritySettings: '/settings/privacy-and-security',
ContentAndMediaSettings: '/settings/content-and-media', ContentAndMediaSettings: '/settings/content-and-media',
SettingsInterests: '/settings/interests', InterestsSettings: '/settings/interests',
AboutSettings: '/settings/about', AboutSettings: '/settings/about',
AppIconSettings: '/settings/app-icon', 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: '/support', Support: '/support',
PrivacyPolicy: '/support/privacy', PrivacyPolicy: '/support/privacy',
+2 -2
View File
@@ -2,9 +2,9 @@ import {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {isNative} from '#/platform/detection'
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
@@ -2,9 +2,11 @@ import {useMemo, useState} from 'react'
import {type TextStyle, View, type ViewStyle} from 'react-native' import {type TextStyle, View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import { import {
preferencesQueryKey, preferencesQueryKey,
usePreferencesQuery, usePreferencesQuery,
@@ -24,7 +26,8 @@ import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function SettingsInterests() { type Props = NativeStackScreenProps<CommonNavigatorParams, 'InterestsSettings'>
export function InterestsSettingsScreen({}: Props) {
const t = useTheme() const t = useTheme()
const gutters = useGutters(['base']) const gutters = useGutters(['base'])
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
@@ -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
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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>
)
}
@@ -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`)
}
+9
View File
@@ -36,6 +36,7 @@ import {AvatarStackWithFetch} from '#/components/AvatarStack'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 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 {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
@@ -180,6 +181,14 @@ export function SettingsScreen({}: Props) {
<Trans>Moderation</Trans> <Trans>Moderation</Trans>
</SettingsList.ItemText> </SettingsList.ItemText>
</SettingsList.LinkItem> </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 <SettingsList.LinkItem
to="/settings/content-and-media" to="/settings/content-and-media"
label={_(msg`Content and media`)}> label={_(msg`Content and media`)}>
+45 -54
View File
@@ -1,72 +1,63 @@
import {msg} from '@lingui/macro' import {type AppBskyNotificationDefs} from '@atproto/api'
import {useLingui} from '@lingui/react' import {t} from '@lingui/macro'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {until} from '#/lib/async/until'
import {logger} from '#/logger' 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 {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
export function useNotificationSettingsMutation() { const RQKEY_ROOT = 'notification-settings'
const {_} = useLingui() 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 agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: async (keys: string[]) => { mutationFn: async (
const enabled = keys[0] === 'enabled' update: Partial<AppBskyNotificationDefs.Preferences>,
) => {
await agent.api.app.bsky.notification.putPreferences({ const response = await agent.app.bsky.notification.putPreferencesV2(
priority: enabled, update,
})
await until(
5, // 5 tries
1e3, // 1s delay between tries
res => res.data.priority === enabled,
() => agent.api.app.bsky.notification.listNotifications({limit: 1}),
) )
return response.data.preferences
eagerlySetCachedPriority(queryClient, enabled)
}, },
onError: err => { onMutate: update => {
logger.error('Failed to save notification preferences', { optimisticUpdateNotificationSettings(queryClient, update)
safeMessage: err,
})
Toast.show(
_(msg`Failed to save notification preferences, please try again`),
'xmark',
)
}, },
onSuccess: () => { onError: e => {
Toast.show(_(msg({message: 'Preference saved', context: 'toast'}))) logger.error('Could not update notification settings', {message: e})
}, queryClient.invalidateQueries({queryKey: RQKEY})
onSettled: () => { Toast.show(t`Could not update notification settings`, 'xmark')
invalidateCachedUnreadPage()
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')})
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')})
}, },
}) })
} }
function eagerlySetCachedPriority( function optimisticUpdateNotificationSettings(
queryClient: ReturnType<typeof useQueryClient>, queryClient: QueryClient,
enabled: boolean, update: Partial<AppBskyNotificationDefs.Preferences>,
) { ) {
function updateData(old: any) { queryClient.setQueryData(
if (!old) return old RQKEY,
return { (old?: AppBskyNotificationDefs.Preferences) => {
...old, if (!old) return old
pages: old.pages.map((page: any) => { return {...old, ...update}
return { },
...page, )
priority: enabled,
}
}),
}
}
queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData)
queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData)
} }
+1 -1
View File
@@ -130,7 +130,7 @@ export function NotificationsScreen({}: Props) {
</Layout.Header.Content> </Layout.Header.Content>
<Layout.Header.Slot> <Layout.Header.Slot>
<Link <Link
to="/notifications/settings" to={{screen: 'NotificationSettings'}}
label={_(msg`Notification settings`)} label={_(msg`Notification settings`)}
size="small" size="small"
variant="ghost" variant="ghost"
+93 -106
View File
@@ -55,13 +55,13 @@
resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz#f39098747dabf8a245d0ed6edc50f362aa4d95f8" resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz#f39098747dabf8a245d0ed6edc50f362aa4d95f8"
integrity sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA== integrity sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA==
"@atproto-labs/xrpc-utils@0.0.14": "@atproto-labs/xrpc-utils@0.0.16":
version "0.0.14" version "0.0.16"
resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.14.tgz#eefe1ccf61a4288708601324496b0106d5ed4ae3" resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.16.tgz#f76c4f615685c60997401f052cbd9f0145d12576"
integrity sha512-/f0Dhzi08w3Oqv38wdwQ5bw238GbxhYIcxg08kVReEMTlkyRDC6H5RuqHf8Ff9J3FKqjKHGdxaOdrPNM1hCgeQ== integrity sha512-WvTQhGjIhFrd/0pMGecE7Xn8BtvvKAgVlNs8UaE6CVRifiCOIvIBwlx1vnslJAavK3FtwL1kKkUdxNtxHciZSQ==
dependencies: dependencies:
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.8.0"
"@atproto/api@^0.15.15": "@atproto/api@^0.15.15":
version "0.15.15" version "0.15.15"
@@ -77,20 +77,6 @@
tlds "^1.234.0" tlds "^1.234.0"
zod "^3.23.8" 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": "@atproto/aws@^0.2.21":
version "0.2.21" version "0.2.21"
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.21.tgz#06006a101c8004db11384a19366296cd87468326" resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.21.tgz#06006a101c8004db11384a19366296cd87468326"
@@ -108,23 +94,23 @@
multiformats "^9.9.0" multiformats "^9.9.0"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/bsky@^0.0.151": "@atproto/bsky@^0.0.159":
version "0.0.151" version "0.0.159"
resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.151.tgz#a0e5b59e163a3b74379fb547601be4fc66b7a133" resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.159.tgz#6fcfd7d6c73e4041c5abc9ac3b99dbe0e1e6cf76"
integrity sha512-42pvUsyGw0nR6Sxlda824maY4gBxUni1cXPG+7uGe6Ixm6XAaPhfTgT1rAg++1rDXH9tT1EXAVnMxg38S6osLg== integrity sha512-kRjDCW6FbByeafrEoUD5YMhhjuKTvSqrE2/QJ5xe9CP8UIhl8BShm2PcBh9gJtYc7lO83aJPqDSqb5gJwNAJUg==
dependencies: dependencies:
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
"@atproto-labs/xrpc-utils" "0.0.14" "@atproto-labs/xrpc-utils" "0.0.16"
"@atproto/api" "^0.15.9" "@atproto/api" "^0.15.15"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/did" "^0.1.5" "@atproto/did" "^0.1.5"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/repo" "^0.8.1" "@atproto/repo" "^0.8.1"
"@atproto/sync" "^0.1.23" "@atproto/sync" "^0.1.25"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.8.0"
"@bufbuild/protobuf" "^1.5.0" "@bufbuild/protobuf" "^1.5.0"
"@connectrpc/connect" "^1.1.4" "@connectrpc/connect" "^1.1.4"
"@connectrpc/connect-express" "^1.1.4" "@connectrpc/connect-express" "^1.1.4"
@@ -154,10 +140,10 @@
uint8arrays "3.0.0" uint8arrays "3.0.0"
undici "^6.19.8" undici "^6.19.8"
"@atproto/bsync@^0.0.19": "@atproto/bsync@^0.0.20":
version "0.0.19" version "0.0.20"
resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.19.tgz#bab3d5e4e7c1ca8de16d9b5efebc49dde12d7160" resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.20.tgz#7409c39a1d4715a5be4ce4a545155de238fc9469"
integrity sha512-AF9aWbU0VlpT//lIuYKhNRplTv+99ld58kfHTS8jfXCpiOZwxwneTkB1hzE+slXJ63K8i/GyzsQCyvRHWzGWCQ== integrity sha512-KbHoZcFpKY869dMQRZXrOXccMkndgLiDY4sfLkCTgp/A0pWw3CKuJmQSmtKHIkWcVkOnfsfJW0J/SgyrXLrn9Q==
dependencies: dependencies:
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
@@ -232,23 +218,23 @@
"@noble/hashes" "^1.6.1" "@noble/hashes" "^1.6.1"
uint8arrays "3.0.0" uint8arrays "3.0.0"
"@atproto/dev-env@^0.3.133": "@atproto/dev-env@^0.3.142":
version "0.3.133" version "0.3.142"
resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.133.tgz#4ca58c9c4c99f001f26ce50629214f81d6acd3ab" resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.142.tgz#466cff00c92d53ad148709ae50bcca816fcee1bc"
integrity sha512-GtKDa+q0Fx2tJZL44cDAINMCxNmt1aKkGVpW/6PTnuSSjdA7ErBUEL3opbwgaAcPRGZfscB0mQmGfWR0BUmvUw== integrity sha512-NiNb3Pdj93goEmKBIF5rIlLSmkfZiwnYmo7U6cGvGXoopRNG5e4Vm2PXb1n7uVdzuvEtiPtpswQxaf0x6jJsWA==
dependencies: dependencies:
"@atproto/api" "^0.15.9" "@atproto/api" "^0.15.15"
"@atproto/bsky" "^0.0.151" "@atproto/bsky" "^0.0.159"
"@atproto/bsync" "^0.0.19" "@atproto/bsync" "^0.0.20"
"@atproto/common-web" "^0.4.2" "@atproto/common-web" "^0.4.2"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/ozone" "^0.1.112" "@atproto/ozone" "^0.1.120"
"@atproto/pds" "^0.4.139" "@atproto/pds" "^0.4.148"
"@atproto/sync" "^0.1.23" "@atproto/sync" "^0.1.25"
"@atproto/syntax" "^0.4.0" "@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/lib" "^0.0.1"
"@did-plc/server" "^0.0.1" "@did-plc/server" "^0.0.1"
dotenv "^16.0.3" dotenv "^16.0.3"
@@ -258,7 +244,7 @@
uint8arrays "3.0.0" uint8arrays "3.0.0"
undici "^6.14.1" undici "^6.14.1"
"@atproto/did@^0.1.5": "@atproto/did@0.1.5", "@atproto/did@^0.1.5":
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.1.5.tgz#5bfe73625d54c4c687c00ff370971ce01c39bd61" resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.1.5.tgz#5bfe73625d54c4c687c00ff370971ce01c39bd61"
integrity sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ== integrity sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==
@@ -273,18 +259,18 @@
"@atproto/common-web" "^0.4.2" "@atproto/common-web" "^0.4.2"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/jwk-jose@0.1.6": "@atproto/jwk-jose@0.1.8":
version "0.1.6" version "0.1.8"
resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.6.tgz#e9fc5a714cd9fe0589f931f3b406585716b5ac03" resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.8.tgz#2dc8ad2cc900e7bc231add293f6518b06dc017ec"
integrity sha512-r4DGMvvmazy6CxqAcnplpUxvp6Vd8UwKxQBZRpmm1aNsVonf5qj1yeDkECTiwoe/FPbvtdamlzClB3UZc7Yb5w== integrity sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA==
dependencies: dependencies:
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.3.0"
jose "^5.2.0" jose "^5.2.0"
"@atproto/jwk@0.1.5": "@atproto/jwk@0.3.0":
version "0.1.5" version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.1.5.tgz#d1e2650431f7f09ed80be48f05908bcd136c2606" resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.3.0.tgz#275fa676f6b5988ddedf4ee0475dd285de9b831b"
integrity sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg== integrity sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ==
dependencies: dependencies:
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
@@ -300,32 +286,32 @@
multiformats "^9.9.0" multiformats "^9.9.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/oauth-provider-api@0.1.2": "@atproto/oauth-provider-api@0.1.4":
version "0.1.2" version "0.1.4"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.2.tgz#cdca03af4426f8cf9b09bc9eb57a8604f9513831" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.4.tgz#a775182e3648dc693a04e3cb604eb62cd9ddfd8c"
integrity sha512-tNAuMrE6D3696euavxo1+Jh7Re0PPwJstbyY8SrdVPXgKJh/LrbpKUKiPNW/p5KyVfRs2tWeAxy+ReESu6SmXA== integrity sha512-3PRrf0gTAVMCETjtIH/3AaQaHBDbjsRBc/OYrlWBZ9IPplchBXtQGH/KcnjE4kK2Ef8p45qQSl3dNWg3EXsbHQ==
dependencies: dependencies:
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.3.0"
"@atproto/oauth-types" "0.2.7" "@atproto/oauth-types" "0.3.0"
"@atproto/oauth-provider-frontend@0.1.5": "@atproto/oauth-provider-frontend@0.1.8":
version "0.1.5" version "0.1.8"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.5.tgz#66fd8760fade2ac94111ad5389f33f4d8ce5bba2" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.8.tgz#21d944566c63f54524f239a10f7c65d150982f40"
integrity sha512-FdDBuwy827+etjIcRwZU7dtxa8Ltso3ufVLMEi8A2V91v21XDysZjLANC6cvmNNSUcS4E/J6ZAwTrQDo7O5axw== integrity sha512-uqfHv+n2xq7vTpuBP1Red7PhpaAbbJbwSbRsSfplJQ16XmF5NCMU8dHGCGRTEHngLZ9UquuIefN3w1QTrNzD0w==
optionalDependencies: optionalDependencies:
"@atproto/oauth-provider-api" "0.1.2" "@atproto/oauth-provider-api" "0.1.4"
"@atproto/oauth-provider-ui@0.1.6": "@atproto/oauth-provider-ui@0.1.9":
version "0.1.6" version "0.1.9"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.6.tgz#4bae995ff57671ac3915f58fdb2cf6a76a0fe42d" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.9.tgz#8c43a1affa94ecb537072e6d569b8a24cdd42e72"
integrity sha512-pJzV9ouNj1/TDUCl3CWEZrHoUese4lcKx5F59t2OiLFm2K7T7QrszKUIMyU5QdiQHv551B0ZJOkJ8+4b/fVGPA== integrity sha512-a6/VAeQWRMxpgnqo/TuqXg3EW2tO68jLh8Mv1uyV1NiZbT7fNlgkII/djIl3fLoEa95I3p236NZxjhKELSBbGg==
optionalDependencies: optionalDependencies:
"@atproto/oauth-provider-api" "0.1.2" "@atproto/oauth-provider-api" "0.1.4"
"@atproto/oauth-provider@^0.7.8": "@atproto/oauth-provider@^0.9.0":
version "0.7.8" version "0.9.0"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.8.tgz#287b15eb6b0bc0bb4b2da2339150253db006c6e0" resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.9.0.tgz#3598924978c2e3d5fdf62bced54574156d15cf92"
integrity sha512-+dEU9dTyfWKeZ/Nu7ocR6fO73RcG0vwDjT45vgcnM9L7jtuPk9zfpmiR4ODYBk9QUu2DURo9yBhtXNJI3Yz8aQ== integrity sha512-LbZS9rbR5l9gVO97wJ3ls+ENXwv6BakmArRyjc5EfaQ4Xc3eLbvE629hpu9LV8LyCkBpseum0l+D+rYXsemNUw==
dependencies: dependencies:
"@atproto-labs/fetch" "0.2.3" "@atproto-labs/fetch" "0.2.3"
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
@@ -333,12 +319,13 @@
"@atproto-labs/simple-store" "0.2.0" "@atproto-labs/simple-store" "0.2.0"
"@atproto-labs/simple-store-memory" "0.1.3" "@atproto-labs/simple-store-memory" "0.1.3"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/jwk" "0.1.5" "@atproto/did" "0.1.5"
"@atproto/jwk-jose" "0.1.6" "@atproto/jwk" "0.3.0"
"@atproto/oauth-provider-api" "0.1.2" "@atproto/jwk-jose" "0.1.8"
"@atproto/oauth-provider-frontend" "0.1.5" "@atproto/oauth-provider-api" "0.1.4"
"@atproto/oauth-provider-ui" "0.1.6" "@atproto/oauth-provider-frontend" "0.1.8"
"@atproto/oauth-types" "0.2.7" "@atproto/oauth-provider-ui" "0.1.9"
"@atproto/oauth-types" "0.3.0"
"@atproto/syntax" "0.4.0" "@atproto/syntax" "0.4.0"
"@hapi/accept" "^6.0.3" "@hapi/accept" "^6.0.3"
"@hapi/address" "^5.1.1" "@hapi/address" "^5.1.1"
@@ -352,27 +339,27 @@
jose "^5.2.0" jose "^5.2.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/oauth-types@0.2.7": "@atproto/oauth-types@0.3.0":
version "0.2.7" version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.7.tgz#c210868052f8babd98510c19816e3d9a156b33c7" resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.3.0.tgz#8d49d939486ac281bc13d0b1fe4462b7e519fdf0"
integrity sha512-2SlDveiSI0oowC+sfuNd/npV8jw/FhokSS26qyUyldTg1g9ZlhxXUfMP4IZOPeZcVn9EszzQRHs1H9ZJqVQIew== integrity sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA==
dependencies: dependencies:
"@atproto/jwk" "0.1.5" "@atproto/jwk" "0.3.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/ozone@^0.1.112": "@atproto/ozone@^0.1.120":
version "0.1.112" version "0.1.120"
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.112.tgz#6b6b5ac052dd4e6dfec3c88f83c9b53f4902fcbe" resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.120.tgz#b5c431a538558179de0465cfbfea512d65e092bf"
integrity sha512-Euut64N/4UyRXyV6m1ATE9K6o6EpCf46ozD4GG8HJ9AC5zEgBYMSkH4l6SLrhKrYYIGXkvglk1WYuuDQKYb3LA== integrity sha512-zu2f16K/z/3r4mC4z/8qISPt0j+Y0GwtjmSE+VOJvVT363iOd9a834K+QHJqnD6B3iTBHR1VPlZ/4fsZ3+4UaA==
dependencies: dependencies:
"@atproto/api" "^0.15.9" "@atproto/api" "^0.15.15"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.8.0"
"@did-plc/lib" "^0.0.1" "@did-plc/lib" "^0.0.1"
compression "^1.7.4" compression "^1.7.4"
cors "^2.8.5" cors "^2.8.5"
@@ -390,24 +377,24 @@
undici "^6.14.1" undici "^6.14.1"
ws "^8.12.0" ws "^8.12.0"
"@atproto/pds@^0.4.139": "@atproto/pds@^0.4.148":
version "0.4.139" version "0.4.148"
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.139.tgz#70ae5afd7d90eab214c652d57a5e6478af454fbe" resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.148.tgz#7af82480e42174ea1c284f5975bf0aff3bd4da24"
integrity sha512-VD1VTSAnbAme4D4Xk/Wdl05qs8YbCe39/i960EyXzw2fYNvL9jMpKm3z0lwhrYN9q7phFhr2ubU2QjfRFDbDAQ== integrity sha512-PbxTpxRAcsdu3zANjwNH+Pfbu0pfj5z6UDmcnc3eaEP11xsExu3+B84jOYKAkIN/PbM1A9EbiBjKb95yBQxGAw==
dependencies: dependencies:
"@atproto-labs/fetch-node" "0.1.9" "@atproto-labs/fetch-node" "0.1.9"
"@atproto-labs/xrpc-utils" "0.0.14" "@atproto-labs/xrpc-utils" "0.0.16"
"@atproto/api" "^0.15.9" "@atproto/api" "^0.15.15"
"@atproto/aws" "^0.2.21" "@atproto/aws" "^0.2.21"
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/oauth-provider" "^0.7.8" "@atproto/oauth-provider" "^0.9.0"
"@atproto/repo" "^0.8.1" "@atproto/repo" "^0.8.1"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc" "^0.7.0" "@atproto/xrpc" "^0.7.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.8.0"
"@did-plc/lib" "^0.0.4" "@did-plc/lib" "^0.0.4"
"@hapi/address" "^5.1.1" "@hapi/address" "^5.1.1"
better-sqlite3 "^10.0.0" better-sqlite3 "^10.0.0"
@@ -452,17 +439,17 @@
varint "^6.0.0" varint "^6.0.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/sync@^0.1.23": "@atproto/sync@^0.1.25":
version "0.1.23" version "0.1.25"
resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.23.tgz#01d4ecf9d5ddc624d14e8fb98927f0b2a97eafeb" resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.25.tgz#66b3453e3cf0ba6a155dbbf207ea46d632c2d6b0"
integrity sha512-1ItRNHMLMcBeTziOZpxS4Q+ha2enQce3fSiAQaCpLCQ8VTNq1D1aRR6ePZCQFzab9jDDtBz0v4FufOnMByRIeg== integrity sha512-4UsQgQsUK+hKFAEDi10Ops6n2W/kfk2JYP8AU6FSHAzOadB1hKRDJbGF5vLiLP9ACBhCzoJerZ31DCnhjzRzfw==
dependencies: dependencies:
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/identity" "^0.4.8" "@atproto/identity" "^0.4.8"
"@atproto/lexicon" "^0.4.11" "@atproto/lexicon" "^0.4.11"
"@atproto/repo" "^0.8.1" "@atproto/repo" "^0.8.1"
"@atproto/syntax" "^0.4.0" "@atproto/syntax" "^0.4.0"
"@atproto/xrpc-server" "^0.7.18" "@atproto/xrpc-server" "^0.8.0"
multiformats "^9.9.0" multiformats "^9.9.0"
p-queue "^6.6.2" p-queue "^6.6.2"
ws "^8.12.0" ws "^8.12.0"
@@ -472,10 +459,10 @@
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2"
integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA== integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA==
"@atproto/xrpc-server@^0.7.18": "@atproto/xrpc-server@^0.8.0":
version "0.7.18" version "0.8.0"
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.18.tgz#7cb6e517da2afec1c9bee70d92c07667a80718ec" resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.8.0.tgz#a32c9c71411ec6ee476fcd0260d5e9e80be348bd"
integrity sha512-kjlAsI+UNbbm6AK3Y5Hb4BJ7VQHNKiYYu2kX5vhZJZHO8qfO40GPYYb/2TknZV8IG6fDPBQhUpcDRolI86sgag== integrity sha512-jDAEVHVhM4IvC0y491gXBuD4b1D9/XrM3HaEronRneAdNZ0qE0nsiJNqiHfQ6r4BvFdHnABM9KyHV9EQTvmxfg==
dependencies: dependencies:
"@atproto/common" "^0.4.11" "@atproto/common" "^0.4.11"
"@atproto/crypto" "^0.4.4" "@atproto/crypto" "^0.4.4"