Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

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>

authored by samuel.fm

surfdude29 and committed by
GitHub
21989b55 7dc6bb57

+1434 -287
+1
assets/icons/bellRinging_stroke2_corner0_rounded.svg
··· 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>
+1
assets/icons/likeRepost_stroke2_corner2_rounded.svg
··· 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>
+1
assets/icons/phoneHaptic_stroke2_corner2_rounded.svg
··· 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>
+1
assets/icons/repostRepost_stroke2_corner2_rounded.svg
··· 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>
+11
bskyweb/cmd/bskyweb/server.go
··· 278 278 e.GET("/settings/content-and-media", server.WebGeneric) 279 279 e.GET("/settings/interests", server.WebGeneric) 280 280 e.GET("/settings/about", server.WebGeneric) 281 + e.GET("/settings/notifications", server.WebGeneric) 282 + e.GET("/settings/notifications/replies", server.WebGeneric) 283 + e.GET("/settings/notifications/mentions", server.WebGeneric) 284 + e.GET("/settings/notifications/quotes", server.WebGeneric) 285 + e.GET("/settings/notifications/likes", server.WebGeneric) 286 + e.GET("/settings/notifications/reposts", server.WebGeneric) 287 + e.GET("/settings/notifications/new-followers", server.WebGeneric) 288 + e.GET("/settings/notifications/likes-on-reposts", server.WebGeneric) 289 + e.GET("/settings/notifications/reposts-on-reposts", server.WebGeneric) 290 + e.GET("/settings/notifications/activity", server.WebGeneric) 291 + e.GET("/settings/notifications/miscellaneous", server.WebGeneric) 281 292 e.GET("/settings/app-icon", server.WebGeneric) 282 293 e.GET("/sys/debug", server.WebGeneric) 283 294 e.GET("/sys/debug-mod", server.WebGeneric)
+1 -1
package.json
··· 218 218 "zod": "^3.20.2" 219 219 }, 220 220 "devDependencies": { 221 - "@atproto/dev-env": "^0.3.133", 221 + "@atproto/dev-env": "^0.3.142", 222 222 "@babel/core": "^7.26.0", 223 223 "@babel/preset-env": "^7.26.0", 224 224 "@babel/runtime": "^7.26.0",
+93 -6
src/Navigation.tsx
··· 90 90 import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' 91 91 import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 92 92 import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 93 + import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' 93 94 import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' 94 - import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' 95 95 import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 96 96 import {SettingsScreen} from '#/screens/Settings/Settings' 97 - import {SettingsInterests} from '#/screens/Settings/SettingsInterests' 98 97 import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' 99 98 import { 100 99 StarterPackScreen, ··· 110 109 } from '#/components/dialogs/EmailDialog' 111 110 import {router} from '#/routes' 112 111 import {Referrer} from '../modules/expo-bluesky-swiss-army' 112 + import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' 113 + import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' 114 + import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' 115 + import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' 116 + import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' 117 + import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' 118 + import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' 119 + import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' 120 + import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' 121 + import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' 122 + import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' 113 123 114 124 const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 115 125 ··· 381 391 }} 382 392 /> 383 393 <Stack.Screen 394 + name="NotificationSettings" 395 + getComponent={() => NotificationSettingsScreen} 396 + options={{title: title(msg`Notification settings`), requireAuth: true}} 397 + /> 398 + <Stack.Screen 399 + name="ReplyNotificationSettings" 400 + getComponent={() => ReplyNotificationSettingsScreen} 401 + options={{ 402 + title: title(msg`Reply notifications`), 403 + requireAuth: true, 404 + }} 405 + /> 406 + <Stack.Screen 407 + name="MentionNotificationSettings" 408 + getComponent={() => MentionNotificationSettingsScreen} 409 + options={{ 410 + title: title(msg`Mention notifications`), 411 + requireAuth: true, 412 + }} 413 + /> 414 + <Stack.Screen 415 + name="QuoteNotificationSettings" 416 + getComponent={() => QuoteNotificationSettingsScreen} 417 + options={{ 418 + title: title(msg`Quote notifications`), 419 + requireAuth: true, 420 + }} 421 + /> 422 + <Stack.Screen 423 + name="LikeNotificationSettings" 424 + getComponent={() => LikeNotificationSettingsScreen} 425 + options={{ 426 + title: title(msg`Like notifications`), 427 + requireAuth: true, 428 + }} 429 + /> 430 + <Stack.Screen 431 + name="RepostNotificationSettings" 432 + getComponent={() => RepostNotificationSettingsScreen} 433 + options={{ 434 + title: title(msg`Repost notifications`), 435 + requireAuth: true, 436 + }} 437 + /> 438 + <Stack.Screen 439 + name="NewFollowerNotificationSettings" 440 + getComponent={() => NewFollowerNotificationSettingsScreen} 441 + options={{ 442 + title: title(msg`New follower notifications`), 443 + requireAuth: true, 444 + }} 445 + /> 446 + <Stack.Screen 447 + name="LikesOnRepostsNotificationSettings" 448 + getComponent={() => LikesOnRepostsNotificationSettingsScreen} 449 + options={{ 450 + title: title(msg`Likes on your reposts notifications`), 451 + requireAuth: true, 452 + }} 453 + /> 454 + <Stack.Screen 455 + name="RepostsOnRepostsNotificationSettings" 456 + getComponent={() => RepostsOnRepostsNotificationSettingsScreen} 457 + options={{ 458 + title: title(msg`Reposts on your reposts notifications`), 459 + requireAuth: true, 460 + }} 461 + /> 462 + <Stack.Screen 463 + name="MiscellaneousNotificationSettings" 464 + getComponent={() => MiscellaneousNotificationSettingsScreen} 465 + options={{ 466 + title: title(msg`Miscellaneous notifications`), 467 + requireAuth: true, 468 + }} 469 + /> 470 + <Stack.Screen 384 471 name="ContentAndMediaSettings" 385 472 getComponent={() => ContentAndMediaSettingsScreen} 386 473 options={{ ··· 389 476 }} 390 477 /> 391 478 <Stack.Screen 392 - name="SettingsInterests" 393 - getComponent={() => SettingsInterests} 479 + name="InterestsSettings" 480 + getComponent={() => InterestsSettingsScreen} 394 481 options={{ 395 482 title: title(msg`Your interests`), 396 483 requireAuth: true, ··· 438 525 options={{title: title(msg`Chat request inbox`), requireAuth: true}} 439 526 /> 440 527 <Stack.Screen 441 - name="NotificationSettings" 442 - getComponent={() => NotificationSettingsScreen} 528 + name="LegacyNotificationSettings" 529 + getComponent={() => LegacyNotificationSettingsScreen} 443 530 options={{title: title(msg`Notification settings`), requireAuth: true}} 444 531 /> 445 532 <Stack.Screen
+5
src/components/icons/BellRinging.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const BellRinging_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+4
src/components/icons/Heart2.tsx
··· 7 7 export const Heart2_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ 8 8 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', 9 9 }) 10 + 11 + export const LikeRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ 12 + 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', 13 + })
+4
src/components/icons/Phone.tsx
··· 3 3 export const Phone_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 4 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', 5 5 }) 6 + 7 + export const PhoneHaptic_Stroke2_Corner2_Rounded = createSinglePathSVG({ 8 + 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', 9 + })
+4
src/components/icons/Repost.tsx
··· 11 11 export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({ 12 12 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', 13 13 }) 14 + 15 + export const RepostRepost_Stroke2_Corner2_Rounded = createSinglePathSVG({ 16 + 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', 17 + })
+3 -3
src/lib/routes/router.ts
··· 1 1 import {type Route, type RouteParams} from './types' 2 2 3 - export class Router { 3 + export class Router<T extends Record<string, any>> { 4 4 routes: [string, Route][] = [] 5 - constructor(description: Record<string, string | string[]>) { 5 + constructor(description: Record<keyof T, string | string[]>) { 6 6 for (const [screen, pattern] of Object.entries(description)) { 7 7 if (typeof pattern === 'string') { 8 8 this.routes.push([screen, createRoute(pattern)]) ··· 14 14 } 15 15 } 16 16 17 - matchName(name: string): Route | undefined { 17 + matchName(name: keyof T | (string & {})): Route | undefined { 18 18 for (const [screenName, route] of this.routes) { 19 19 if (screenName === name) { 20 20 return route
+13 -11
src/lib/routes/types.ts
··· 52 52 AccountSettings: undefined 53 53 PrivacyAndSecuritySettings: undefined 54 54 ContentAndMediaSettings: undefined 55 - SettingsInterests: undefined 55 + NotificationSettings: undefined 56 + ReplyNotificationSettings: undefined 57 + MentionNotificationSettings: undefined 58 + QuoteNotificationSettings: undefined 59 + LikeNotificationSettings: undefined 60 + RepostNotificationSettings: undefined 61 + NewFollowerNotificationSettings: undefined 62 + LikesOnRepostsNotificationSettings: undefined 63 + RepostsOnRepostsNotificationSettings: undefined 64 + ActivityNotificationSettings: undefined 65 + MiscellaneousNotificationSettings: undefined 66 + InterestsSettings: undefined 56 67 AboutSettings: undefined 57 68 AppIconSettings: undefined 58 69 Search: {q?: string} ··· 61 72 MessagesConversation: {conversation: string; embed?: string; accept?: true} 62 73 MessagesSettings: undefined 63 74 MessagesInbox: undefined 64 - NotificationSettings: undefined 75 + LegacyNotificationSettings: undefined 65 76 Feeds: undefined 66 77 Start: {name: string; rkey: string} 67 78 StarterPack: {name: string; rkey: string; new?: boolean} ··· 104 115 Search: {q?: string} 105 116 Feeds: undefined 106 117 Notifications: undefined 107 - Hashtag: {tag: string; author?: string} 108 - Topic: {topic: string} 109 118 Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} 110 119 } 111 120 ··· 118 127 NotificationsTab: undefined 119 128 Notifications: undefined 120 129 MyProfileTab: undefined 121 - Hashtag: {tag: string; author?: string} 122 - Topic: {topic: string} 123 130 MessagesTab: undefined 124 131 Messages: {animation?: 'push' | 'pop'} 125 - Start: {name: string; rkey: string} 126 - StarterPack: {name: string; rkey: string; new?: boolean} 127 - StarterPackShort: {code: string} 128 - StarterPackWizard: undefined 129 - StarterPackEdit: {rkey?: string} 130 132 } 131 133 132 134 // NOTE
+22 -4
src/routes.ts
··· 1 1 import {Router} from '#/lib/routes/router' 2 + import {type FlatNavigatorParams} from './lib/routes/types' 2 3 3 - export const router = new Router({ 4 + type AllNavigatableRoutes = Omit< 5 + FlatNavigatorParams, 6 + 'NotFound' | 'SharedPreferencesTester' 7 + > 8 + 9 + export const router = new Router<AllNavigatableRoutes>({ 4 10 Home: '/', 5 11 Search: '/search', 6 12 Feeds: '/feeds', 7 13 Notifications: '/notifications', 8 - NotificationSettings: '/notifications/settings', 14 + LegacyNotificationSettings: '/notifications/settings', 9 15 Settings: '/settings', 10 16 Lists: '/lists', 11 17 // moderation ··· 42 48 AccessibilitySettings: '/settings/accessibility', 43 49 AppearanceSettings: '/settings/appearance', 44 50 SavedFeeds: '/settings/saved-feeds', 45 - // new settings 46 51 AccountSettings: '/settings/account', 47 52 PrivacyAndSecuritySettings: '/settings/privacy-and-security', 48 53 ContentAndMediaSettings: '/settings/content-and-media', 49 - SettingsInterests: '/settings/interests', 54 + InterestsSettings: '/settings/interests', 50 55 AboutSettings: '/settings/about', 51 56 AppIconSettings: '/settings/app-icon', 57 + NotificationSettings: '/settings/notifications', 58 + ReplyNotificationSettings: '/settings/notifications/replies', 59 + MentionNotificationSettings: '/settings/notifications/mentions', 60 + QuoteNotificationSettings: '/settings/notifications/quotes', 61 + LikeNotificationSettings: '/settings/notifications/likes', 62 + RepostNotificationSettings: '/settings/notifications/reposts', 63 + NewFollowerNotificationSettings: '/settings/notifications/new-followers', 64 + LikesOnRepostsNotificationSettings: 65 + '/settings/notifications/likes-on-reposts', 66 + RepostsOnRepostsNotificationSettings: 67 + '/settings/notifications/reposts-on-reposts', 68 + ActivityNotificationSettings: '/settings/notifications/activity', 69 + MiscellaneousNotificationSettings: '/settings/notifications/miscellaneous', 52 70 // support 53 71 Support: '/support', 54 72 PrivacyPolicy: '/support/privacy',
+2 -2
src/screens/Messages/Settings.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 - import {NativeStackScreenProps} from '@react-navigation/native-stack' 5 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 6 6 7 - import {CommonNavigatorParams} from '#/lib/routes/types' 7 + import {type CommonNavigatorParams} from '#/lib/routes/types' 8 8 import {isNative} from '#/platform/detection' 9 9 import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' 10 10 import {useProfileQuery} from '#/state/queries/profile'
+21
src/screens/Settings/LegacyNotificationSettings.tsx
··· 1 + import {useCallback} from 'react' 2 + import {useFocusEffect} from '@react-navigation/native' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + 9 + type Props = NativeStackScreenProps< 10 + AllNavigatorParams, 11 + 'LegacyNotificationSettings' 12 + > 13 + export function LegacyNotificationSettingsScreen({navigation}: Props) { 14 + useFocusEffect( 15 + useCallback(() => { 16 + navigation.replace('NotificationSettings') 17 + }, [navigation]), 18 + ) 19 + 20 + return null 21 + }
-98
src/screens/Settings/NotificationSettings.tsx
··· 1 - import {Text} from 'react-native' 2 - import {msg, Trans} from '@lingui/macro' 3 - import {useLingui} from '@lingui/react' 4 - 5 - import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 6 - import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' 7 - import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings' 8 - import {atoms as a} from '#/alf' 9 - import {Admonition} from '#/components/Admonition' 10 - import {Error} from '#/components/Error' 11 - import * as Toggle from '#/components/forms/Toggle' 12 - import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' 13 - import * as Layout from '#/components/Layout' 14 - import {Loader} from '#/components/Loader' 15 - import * as SettingsList from './components/SettingsList' 16 - 17 - type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'> 18 - export function NotificationSettingsScreen({}: Props) { 19 - const {_} = useLingui() 20 - 21 - const { 22 - data, 23 - isError: isQueryError, 24 - refetch, 25 - } = useNotificationFeedQuery({ 26 - filter: 'all', 27 - }) 28 - const serverPriority = data?.pages.at(0)?.priority 29 - 30 - const { 31 - mutate: onChangePriority, 32 - isPending: isMutationPending, 33 - variables, 34 - } = useNotificationSettingsMutation() 35 - 36 - const priority = isMutationPending 37 - ? variables[0] === 'enabled' 38 - : serverPriority 39 - 40 - return ( 41 - <Layout.Screen> 42 - <Layout.Header.Outer> 43 - <Layout.Header.BackButton /> 44 - <Layout.Header.Content> 45 - <Layout.Header.TitleText> 46 - <Trans>Notification Settings</Trans> 47 - </Layout.Header.TitleText> 48 - </Layout.Header.Content> 49 - <Layout.Header.Slot /> 50 - </Layout.Header.Outer> 51 - <Layout.Content> 52 - {isQueryError ? ( 53 - <Error 54 - title={_(msg`Oops!`)} 55 - message={_(msg`Something went wrong!`)} 56 - onRetry={refetch} 57 - sideBorders={false} 58 - /> 59 - ) : ( 60 - <SettingsList.Container> 61 - <SettingsList.Group> 62 - <SettingsList.ItemIcon icon={BeakerIcon} /> 63 - <SettingsList.ItemText> 64 - <Trans>Notification filters</Trans> 65 - </SettingsList.ItemText> 66 - <Toggle.Group 67 - label={_(msg`Priority notifications`)} 68 - type="checkbox" 69 - values={priority ? ['enabled'] : []} 70 - onChange={onChangePriority} 71 - disabled={typeof priority !== 'boolean' || isMutationPending}> 72 - <Toggle.Item 73 - name="enabled" 74 - label={_(msg`Enable priority notifications`)} 75 - style={[a.flex_1, a.justify_between]}> 76 - <Toggle.LabelText> 77 - <Trans>Enable priority notifications</Trans> 78 - </Toggle.LabelText> 79 - {!data ? <Loader size="md" /> : <Toggle.Platform />} 80 - </Toggle.Item> 81 - </Toggle.Group> 82 - </SettingsList.Group> 83 - <SettingsList.Item> 84 - <Admonition type="warning" style={[a.flex_1]}> 85 - <Trans> 86 - <Text style={[a.font_bold]}>Experimental:</Text> When this 87 - preference is enabled, you'll only receive reply and quote 88 - notifications from users you follow. We'll continue to add 89 - more controls here over time. 90 - </Trans> 91 - </Admonition> 92 - </SettingsList.Item> 93 - </SettingsList.Container> 94 - )} 95 - </Layout.Content> 96 - </Layout.Screen> 97 - ) 98 - }
+60
src/screens/Settings/NotificationSettings/LikeNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {Heart2_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'LikeNotificationSettings' 20 + > 21 + export function LikeNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={HeartIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Likes</Trans>} 42 + subtitleText={ 43 + <Trans>Get notifications when people like your posts.</Trans> 44 + } 45 + /> 46 + </SettingsList.Item> 47 + {isError ? ( 48 + <View style={[a.px_lg, a.pt_md]}> 49 + <Admonition type="error"> 50 + <Trans>Failed to load notification settings.</Trans> 51 + </Admonition> 52 + </View> 53 + ) : ( 54 + <PreferenceControls name="like" preference={preferences?.like} /> 55 + )} 56 + </SettingsList.Container> 57 + </Layout.Content> 58 + </Layout.Screen> 59 + ) 60 + }
+65
src/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon} from '#/components/icons/Heart2' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'LikesOnRepostsNotificationSettings' 20 + > 21 + export function LikesOnRepostsNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={LikeRepostIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Likes on your reposts</Trans>} 42 + subtitleText={ 43 + <Trans> 44 + Get notifications when people like posts that you've reposted. 45 + </Trans> 46 + } 47 + /> 48 + </SettingsList.Item> 49 + {isError ? ( 50 + <View style={[a.px_lg, a.pt_md]}> 51 + <Admonition type="error"> 52 + <Trans>Failed to load notification settings.</Trans> 53 + </Admonition> 54 + </View> 55 + ) : ( 56 + <PreferenceControls 57 + name="likeViaRepost" 58 + preference={preferences?.likeViaRepost} 59 + /> 60 + )} 61 + </SettingsList.Container> 62 + </Layout.Content> 63 + </Layout.Screen> 64 + ) 65 + }
+63
src/screens/Settings/NotificationSettings/MentionNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'MentionNotificationSettings' 20 + > 21 + export function MentionNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={AtIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Mentions</Trans>} 42 + subtitleText={ 43 + <Trans>Get notifications when people mention you.</Trans> 44 + } 45 + /> 46 + </SettingsList.Item> 47 + {isError ? ( 48 + <View style={[a.px_lg, a.pt_md]}> 49 + <Admonition type="error"> 50 + <Trans>Failed to load notification settings.</Trans> 51 + </Admonition> 52 + </View> 53 + ) : ( 54 + <PreferenceControls 55 + name="mention" 56 + preference={preferences?.mention} 57 + /> 58 + )} 59 + </SettingsList.Container> 60 + </Layout.Content> 61 + </Layout.Screen> 62 + ) 63 + }
+68
src/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'MiscellaneousNotificationSettings' 20 + > 21 + export function MiscellaneousNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={ShapesIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Everything else</Trans>} 42 + subtitleText={ 43 + <Trans> 44 + Notifications for everything else, such as when someone joins 45 + via one of your starter packs. 46 + </Trans> 47 + } 48 + /> 49 + </SettingsList.Item> 50 + {isError ? ( 51 + <View style={[a.px_lg, a.pt_md]}> 52 + <Admonition type="error"> 53 + <Trans>Failed to load notification settings.</Trans> 54 + </Admonition> 55 + </View> 56 + ) : ( 57 + <PreferenceControls 58 + name="starterpackJoined" 59 + preference={preferences?.starterpackJoined} 60 + syncOthers={['verified', 'unverified']} 61 + allowDisableInApp={false} 62 + /> 63 + )} 64 + </SettingsList.Container> 65 + </Layout.Content> 66 + </Layout.Screen> 67 + ) 68 + }
+63
src/screens/Settings/NotificationSettings/NewFollowerNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'NewFollowerNotificationSettings' 20 + > 21 + export function NewFollowerNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={PersonPlusIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>New followers</Trans>} 42 + subtitleText={ 43 + <Trans>Get notifications when people follow you.</Trans> 44 + } 45 + /> 46 + </SettingsList.Item> 47 + {isError ? ( 48 + <View style={[a.px_lg, a.pt_md]}> 49 + <Admonition type="error"> 50 + <Trans>Failed to load notification settings.</Trans> 51 + </Admonition> 52 + </View> 53 + ) : ( 54 + <PreferenceControls 55 + name="follow" 56 + preference={preferences?.follow} 57 + /> 58 + )} 59 + </SettingsList.Container> 60 + </Layout.Content> 61 + </Layout.Screen> 62 + ) 63 + }
+60
src/screens/Settings/NotificationSettings/QuoteNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'QuoteNotificationSettings' 20 + > 21 + export function QuoteNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={CloseQuoteIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Quotes</Trans>} 42 + subtitleText={ 43 + <Trans>Get notifications when people quote your posts.</Trans> 44 + } 45 + /> 46 + </SettingsList.Item> 47 + {isError ? ( 48 + <View style={[a.px_lg, a.pt_md]}> 49 + <Admonition type="error"> 50 + <Trans>Failed to load notification settings.</Trans> 51 + </Admonition> 52 + </View> 53 + ) : ( 54 + <PreferenceControls name="quote" preference={preferences?.quote} /> 55 + )} 56 + </SettingsList.Container> 57 + </Layout.Content> 58 + </Layout.Screen> 59 + ) 60 + }
+66
src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'ReplyNotificationSettings' 20 + > 21 + export function ReplyNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={BubbleIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Replies</Trans>} 42 + subtitleText={ 43 + <Trans> 44 + Get notifications when people reply to your posts. 45 + </Trans> 46 + } 47 + /> 48 + </SettingsList.Item> 49 + {isError ? ( 50 + <View style={[a.px_lg, a.pt_md]}> 51 + <Admonition type="error"> 52 + <Trans>Failed to load notification settings.</Trans> 53 + </Admonition> 54 + </View> 55 + ) : ( 56 + <PreferenceControls 57 + name="reply" 58 + preference={preferences?.reply} 59 + allowDisableInApp={false} 60 + /> 61 + )} 62 + </SettingsList.Container> 63 + </Layout.Content> 64 + </Layout.Screen> 65 + ) 66 + }
+63
src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'RepostNotificationSettings' 20 + > 21 + export function RepostNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={RepostIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Reposts</Trans>} 42 + subtitleText={ 43 + <Trans>Get notifications when people repost your posts.</Trans> 44 + } 45 + /> 46 + </SettingsList.Item> 47 + {isError ? ( 48 + <View style={[a.px_lg, a.pt_md]}> 49 + <Admonition type="error"> 50 + <Trans>Failed to load notification settings.</Trans> 51 + </Admonition> 52 + </View> 53 + ) : ( 54 + <PreferenceControls 55 + name="repost" 56 + preference={preferences?.repost} 57 + /> 58 + )} 59 + </SettingsList.Container> 60 + </Layout.Content> 61 + </Layout.Screen> 62 + ) 63 + }
+66
src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans} from '@lingui/macro' 3 + 4 + import { 5 + type AllNavigatorParams, 6 + type NativeStackScreenProps, 7 + } from '#/lib/routes/types' 8 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 9 + import {atoms as a} from '#/alf' 10 + import {Admonition} from '#/components/Admonition' 11 + import {RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon} from '#/components/icons/Repost' 12 + import * as Layout from '#/components/Layout' 13 + import * as SettingsList from '../components/SettingsList' 14 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 15 + import {PreferenceControls} from './components/PreferenceControls' 16 + 17 + type Props = NativeStackScreenProps< 18 + AllNavigatorParams, 19 + 'RepostsOnRepostsNotificationSettings' 20 + > 21 + export function RepostsOnRepostsNotificationSettingsScreen({}: Props) { 22 + const {data: preferences, isError} = useNotificationSettingsQuery() 23 + 24 + return ( 25 + <Layout.Screen> 26 + <Layout.Header.Outer> 27 + <Layout.Header.BackButton /> 28 + <Layout.Header.Content> 29 + <Layout.Header.TitleText> 30 + <Trans>Notifications</Trans> 31 + </Layout.Header.TitleText> 32 + </Layout.Header.Content> 33 + <Layout.Header.Slot /> 34 + </Layout.Header.Outer> 35 + <Layout.Content> 36 + <SettingsList.Container> 37 + <SettingsList.Item style={[a.align_start]}> 38 + <SettingsList.ItemIcon icon={RepostRepostIcon} /> 39 + <ItemTextWithSubtitle 40 + bold 41 + titleText={<Trans>Reposts of your reposts</Trans>} 42 + subtitleText={ 43 + <Trans> 44 + Get notifications when people repost posts that you've 45 + reposted. 46 + </Trans> 47 + } 48 + /> 49 + </SettingsList.Item> 50 + {isError ? ( 51 + <View style={[a.px_lg, a.pt_md]}> 52 + <Admonition type="error"> 53 + <Trans>Failed to load notification settings.</Trans> 54 + </Admonition> 55 + </View> 56 + ) : ( 57 + <PreferenceControls 58 + name="repostViaRepost" 59 + preference={preferences?.repostViaRepost} 60 + /> 61 + )} 62 + </SettingsList.Container> 63 + </Layout.Content> 64 + </Layout.Screen> 65 + ) 66 + }
+34
src/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle.tsx
··· 1 + import {View} from 'react-native' 2 + 3 + import {atoms as a, useTheme} from '#/alf' 4 + import * as Skele from '#/components/Skeleton' 5 + import {Text} from '#/components/Typography' 6 + import * as SettingsList from '../../components/SettingsList' 7 + 8 + export function ItemTextWithSubtitle({ 9 + titleText, 10 + subtitleText, 11 + bold = false, 12 + showSkeleton = false, 13 + }: { 14 + titleText: React.ReactNode 15 + subtitleText: React.ReactNode 16 + bold?: boolean 17 + showSkeleton?: boolean 18 + }) { 19 + const t = useTheme() 20 + return ( 21 + <View style={[a.flex_1, bold ? a.gap_xs : a.gap_2xs]}> 22 + <SettingsList.ItemText style={bold && [a.font_bold, a.text_lg]}> 23 + {titleText} 24 + </SettingsList.ItemText> 25 + {showSkeleton ? ( 26 + <Skele.Text style={[a.text_sm, {width: 120}]} /> 27 + ) : ( 28 + <Text style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}> 29 + {subtitleText} 30 + </Text> 31 + )} 32 + </View> 33 + ) 34 + }
+194
src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx
··· 1 + import {useMemo} from 'react' 2 + import {View} from 'react-native' 3 + import {type AppBskyNotificationDefs} from '@atproto/api' 4 + import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky/notification/defs' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 + 8 + import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings' 9 + import {atoms as a, platform, useTheme} from '#/alf' 10 + import * as Toggle from '#/components/forms/Toggle' 11 + import {Loader} from '#/components/Loader' 12 + import {Text} from '#/components/Typography' 13 + import {Divider} from '../../components/SettingsList' 14 + 15 + export function PreferenceControls({ 16 + name, 17 + syncOthers, 18 + preference, 19 + allowDisableInApp = true, 20 + }: { 21 + name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'> 22 + /** 23 + * Keep other prefs in sync with `name`. For use in the "everything else" category 24 + * which groups starterpack joins + verified + unverified notifications into a single toggle. 25 + */ 26 + syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[] 27 + preference?: AppBskyNotificationDefs.Preference | FilterablePreference 28 + allowDisableInApp?: boolean 29 + }) { 30 + if (!preference) 31 + return ( 32 + <View style={[a.w_full, a.pt_5xl, a.align_center]}> 33 + <Loader size="xl" /> 34 + </View> 35 + ) 36 + 37 + return ( 38 + <Inner 39 + name={name} 40 + syncOthers={syncOthers} 41 + preference={preference} 42 + allowDisableInApp={allowDisableInApp} 43 + /> 44 + ) 45 + } 46 + 47 + export function Inner({ 48 + name, 49 + syncOthers = [], 50 + preference, 51 + allowDisableInApp, 52 + }: { 53 + name: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'> 54 + syncOthers?: Exclude<keyof AppBskyNotificationDefs.Preferences, '$type'>[] 55 + preference: AppBskyNotificationDefs.Preference | FilterablePreference 56 + allowDisableInApp: boolean 57 + }) { 58 + const t = useTheme() 59 + const {_} = useLingui() 60 + const {mutate} = useNotificationSettingsUpdateMutation() 61 + 62 + const channels = useMemo(() => { 63 + const arr = [] 64 + if (preference.list) arr.push('list') 65 + if (preference.push) arr.push('push') 66 + return arr 67 + }, [preference]) 68 + 69 + const onChangeChannels = (change: string[]) => { 70 + const newPreference = { 71 + ...preference, 72 + list: change.includes('list'), 73 + push: change.includes('push'), 74 + } satisfies typeof preference 75 + 76 + mutate({ 77 + [name]: newPreference, 78 + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), 79 + }) 80 + } 81 + 82 + const onChangeFilter = ([change]: string[]) => { 83 + if (change !== 'all' && change !== 'follows') 84 + throw new Error('Invalid filter') 85 + 86 + const newPreference = { 87 + ...preference, 88 + filter: change, 89 + } satisfies typeof preference 90 + 91 + mutate({ 92 + [name]: newPreference, 93 + ...Object.fromEntries(syncOthers.map(key => [key, newPreference])), 94 + }) 95 + } 96 + 97 + return ( 98 + <View style={[a.px_xl, a.pt_md, a.gap_sm]}> 99 + <Toggle.Group 100 + type="checkbox" 101 + label={_(`Select your preferred notification channels`)} 102 + values={channels} 103 + onChange={onChangeChannels}> 104 + <View style={[a.gap_sm]}> 105 + <Toggle.Item 106 + label={_(msg`Receive push notifications`)} 107 + name="push" 108 + style={[ 109 + a.py_xs, 110 + platform({ 111 + native: [a.justify_between], 112 + web: [a.flex_row_reverse, a.gap_md], 113 + }), 114 + ]}> 115 + <Toggle.LabelText 116 + style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> 117 + <Trans>Push notifications</Trans> 118 + </Toggle.LabelText> 119 + <Toggle.Platform /> 120 + </Toggle.Item> 121 + {allowDisableInApp && ( 122 + <Toggle.Item 123 + label={_(msg`Receive in-app notifications`)} 124 + name="list" 125 + style={[ 126 + a.py_xs, 127 + platform({ 128 + native: [a.justify_between], 129 + web: [a.flex_row_reverse, a.gap_md], 130 + }), 131 + ]}> 132 + <Toggle.LabelText 133 + style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> 134 + <Trans>In-app notifications</Trans> 135 + </Toggle.LabelText> 136 + <Toggle.Platform /> 137 + </Toggle.Item> 138 + )} 139 + </View> 140 + </Toggle.Group> 141 + {'filter' in preference && ( 142 + <> 143 + <Divider /> 144 + <Text style={[a.font_bold, a.text_md]}>From</Text> 145 + <Toggle.Group 146 + type="radio" 147 + label={_('Filter who you receive notifications from')} 148 + values={[preference.filter]} 149 + onChange={onChangeFilter} 150 + disabled={channels.length === 0}> 151 + <View style={[a.gap_sm]}> 152 + <Toggle.Item 153 + label={_(msg`Everyone`)} 154 + name="all" 155 + style={[ 156 + a.flex_row, 157 + a.py_xs, 158 + platform({native: [a.gap_sm], web: [a.gap_md]}), 159 + ]}> 160 + <Toggle.Radio /> 161 + <Toggle.LabelText 162 + style={[ 163 + channels.length > 0 && t.atoms.text, 164 + a.font_normal, 165 + a.text_md, 166 + ]}> 167 + <Trans>Everyone</Trans> 168 + </Toggle.LabelText> 169 + </Toggle.Item> 170 + <Toggle.Item 171 + label={_(msg`People I follow`)} 172 + name="follows" 173 + style={[ 174 + a.flex_row, 175 + a.py_xs, 176 + platform({native: [a.gap_sm], web: [a.gap_md]}), 177 + ]}> 178 + <Toggle.Radio /> 179 + <Toggle.LabelText 180 + style={[ 181 + channels.length > 0 && t.atoms.text, 182 + a.font_normal, 183 + a.text_md, 184 + ]}> 185 + <Trans>People I follow</Trans> 186 + </Toggle.LabelText> 187 + </Toggle.Item> 188 + </View> 189 + </Toggle.Group> 190 + </> 191 + )} 192 + </View> 193 + ) 194 + }
+293
src/screens/Settings/NotificationSettings/index.tsx
··· 1 + import {useEffect} from 'react' 2 + import {Linking, View} from 'react-native' 3 + import * as Notification from 'expo-notifications' 4 + import {type AppBskyNotificationDefs} from '@atproto/api' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 + import {useQuery, useQueryClient} from '@tanstack/react-query' 8 + 9 + import {useAppState} from '#/lib/hooks/useAppState' 10 + import { 11 + type AllNavigatorParams, 12 + type NativeStackScreenProps, 13 + } from '#/lib/routes/types' 14 + import {isAndroid, isIOS, isWeb} from '#/platform/detection' 15 + import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 16 + import {atoms as a} from '#/alf' 17 + import {Admonition} from '#/components/Admonition' 18 + import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' 19 + // import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging' 20 + import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' 21 + import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' 22 + import { 23 + Heart2_Stroke2_Corner0_Rounded as HeartIcon, 24 + LikeRepost_Stroke2_Corner2_Rounded as LikeRepostIcon, 25 + } from '#/components/icons/Heart2' 26 + import {PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon} from '#/components/icons/Person' 27 + import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' 28 + import { 29 + Repost_Stroke2_Corner2_Rounded as RepostIcon, 30 + RepostRepost_Stroke2_Corner2_Rounded as RepostRepostIcon, 31 + } from '#/components/icons/Repost' 32 + import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' 33 + import * as Layout from '#/components/Layout' 34 + import * as SettingsList from '../components/SettingsList' 35 + import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 36 + 37 + const RQKEY = ['notification-permissions'] 38 + 39 + type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'> 40 + export function NotificationSettingsScreen({}: Props) { 41 + const {_} = useLingui() 42 + const queryClient = useQueryClient() 43 + const {data: settings, isError} = useNotificationSettingsQuery() 44 + 45 + const {data: permissions, refetch} = useQuery({ 46 + queryKey: RQKEY, 47 + queryFn: async () => { 48 + if (isWeb) return null 49 + return await Notification.getPermissionsAsync() 50 + }, 51 + }) 52 + 53 + const appState = useAppState() 54 + useEffect(() => { 55 + if (appState === 'active') { 56 + refetch() 57 + } 58 + }, [appState, refetch]) 59 + 60 + const onRequestPermissions = async () => { 61 + if (isWeb) return 62 + if (permissions?.canAskAgain) { 63 + const response = await Notification.requestPermissionsAsync() 64 + queryClient.setQueryData(RQKEY, response) 65 + } else { 66 + if (isAndroid) { 67 + try { 68 + await Linking.sendIntent( 69 + 'android.settings.APP_NOTIFICATION_SETTINGS', 70 + [ 71 + { 72 + key: 'android.provider.extra.APP_PACKAGE', 73 + value: 'xyz.blueskyweb.app', 74 + }, 75 + ], 76 + ) 77 + } catch { 78 + Linking.openSettings() 79 + } 80 + } else if (isIOS) { 81 + Linking.openSettings() 82 + } 83 + } 84 + } 85 + 86 + return ( 87 + <Layout.Screen> 88 + <Layout.Header.Outer> 89 + <Layout.Header.BackButton /> 90 + <Layout.Header.Content> 91 + <Layout.Header.TitleText> 92 + <Trans>Notifications</Trans> 93 + </Layout.Header.TitleText> 94 + </Layout.Header.Content> 95 + <Layout.Header.Slot /> 96 + </Layout.Header.Outer> 97 + <Layout.Content> 98 + <SettingsList.Container> 99 + {permissions && !permissions.granted && ( 100 + <> 101 + <SettingsList.PressableItem 102 + label={_(msg`Enable push notifications`)} 103 + onPress={onRequestPermissions}> 104 + <SettingsList.ItemIcon icon={HapticIcon} /> 105 + <SettingsList.ItemText> 106 + <Trans>Enable push notifications</Trans> 107 + </SettingsList.ItemText> 108 + </SettingsList.PressableItem> 109 + <SettingsList.Divider /> 110 + </> 111 + )} 112 + {isError && ( 113 + <View style={[a.px_lg, a.pb_md]}> 114 + <Admonition type="error"> 115 + <Trans>Failed to load notification settings.</Trans> 116 + </Admonition> 117 + </View> 118 + )} 119 + <View style={[a.gap_sm]}> 120 + <SettingsList.LinkItem 121 + label={_(msg`Settings for reply notifications`)} 122 + to={{screen: 'ReplyNotificationSettings'}} 123 + contentContainerStyle={[a.align_start]}> 124 + <SettingsList.ItemIcon icon={BubbleIcon} /> 125 + <ItemTextWithSubtitle 126 + titleText={<Trans>Replies</Trans>} 127 + subtitleText={<SettingPreview preference={settings?.reply} />} 128 + showSkeleton={!settings} 129 + /> 130 + </SettingsList.LinkItem> 131 + <SettingsList.LinkItem 132 + label={_(msg`Settings for mention notifications`)} 133 + to={{screen: 'MentionNotificationSettings'}} 134 + contentContainerStyle={[a.align_start]}> 135 + <SettingsList.ItemIcon icon={AtIcon} /> 136 + <ItemTextWithSubtitle 137 + titleText={<Trans>Mentions</Trans>} 138 + subtitleText={<SettingPreview preference={settings?.mention} />} 139 + showSkeleton={!settings} 140 + /> 141 + </SettingsList.LinkItem> 142 + <SettingsList.LinkItem 143 + label={_(msg`Settings for quote notifications`)} 144 + to={{screen: 'QuoteNotificationSettings'}} 145 + contentContainerStyle={[a.align_start]}> 146 + <SettingsList.ItemIcon icon={CloseQuoteIcon} /> 147 + <ItemTextWithSubtitle 148 + titleText={<Trans>Quotes</Trans>} 149 + subtitleText={<SettingPreview preference={settings?.quote} />} 150 + showSkeleton={!settings} 151 + /> 152 + </SettingsList.LinkItem> 153 + <SettingsList.LinkItem 154 + label={_(msg`Settings for like notifications`)} 155 + to={{screen: 'LikeNotificationSettings'}} 156 + contentContainerStyle={[a.align_start]}> 157 + <SettingsList.ItemIcon icon={HeartIcon} /> 158 + <ItemTextWithSubtitle 159 + titleText={<Trans>Likes</Trans>} 160 + subtitleText={<SettingPreview preference={settings?.like} />} 161 + showSkeleton={!settings} 162 + /> 163 + </SettingsList.LinkItem> 164 + <SettingsList.LinkItem 165 + label={_(msg`Settings for repost notifications`)} 166 + to={{screen: 'RepostNotificationSettings'}} 167 + contentContainerStyle={[a.align_start]}> 168 + <SettingsList.ItemIcon icon={RepostIcon} /> 169 + <ItemTextWithSubtitle 170 + titleText={<Trans>Reposts</Trans>} 171 + subtitleText={<SettingPreview preference={settings?.repost} />} 172 + showSkeleton={!settings} 173 + /> 174 + </SettingsList.LinkItem> 175 + <SettingsList.LinkItem 176 + label={_(msg`Settings for new follower notifications`)} 177 + to={{screen: 'NewFollowerNotificationSettings'}} 178 + contentContainerStyle={[a.align_start]}> 179 + <SettingsList.ItemIcon icon={PersonPlusIcon} /> 180 + <ItemTextWithSubtitle 181 + titleText={<Trans>New followers</Trans>} 182 + subtitleText={<SettingPreview preference={settings?.follow} />} 183 + showSkeleton={!settings} 184 + /> 185 + </SettingsList.LinkItem> 186 + {/* <SettingsList.LinkItem 187 + label={_(msg`Settings for activity alerts`)} 188 + to={{screen: 'ActivityNotificationSettings'}} 189 + contentContainerStyle={[a.align_start]}> 190 + <SettingsList.ItemIcon icon={BellRingingIcon} /> 191 + 192 + <ItemTextWithSubtitle 193 + titleText={<Trans>Activity alerts</Trans>} 194 + subtitleText={ 195 + <SettingPreview preference={settings?.subscribedPost} /> 196 + } 197 + showSkeleton={!settings} 198 + /> 199 + </SettingsList.LinkItem> */} 200 + <SettingsList.LinkItem 201 + label={_( 202 + msg`Settings for notifications for likes on your reposts`, 203 + )} 204 + to={{screen: 'LikesOnRepostsNotificationSettings'}} 205 + contentContainerStyle={[a.align_start]}> 206 + <SettingsList.ItemIcon icon={LikeRepostIcon} /> 207 + <ItemTextWithSubtitle 208 + titleText={<Trans>Likes on your reposts</Trans>} 209 + subtitleText={ 210 + <SettingPreview preference={settings?.likeViaRepost} /> 211 + } 212 + showSkeleton={!settings} 213 + /> 214 + </SettingsList.LinkItem> 215 + <SettingsList.LinkItem 216 + label={_( 217 + msg`Settings for notifications for reposts of your reposts`, 218 + )} 219 + to={{screen: 'RepostsOnRepostsNotificationSettings'}} 220 + contentContainerStyle={[a.align_start]}> 221 + <SettingsList.ItemIcon icon={RepostRepostIcon} /> 222 + <ItemTextWithSubtitle 223 + titleText={<Trans>Reposts of your reposts</Trans>} 224 + subtitleText={ 225 + <SettingPreview preference={settings?.repostViaRepost} /> 226 + } 227 + showSkeleton={!settings} 228 + /> 229 + </SettingsList.LinkItem> 230 + <SettingsList.LinkItem 231 + label={_(msg`Settings for notifications for everything else`)} 232 + to={{screen: 'MiscellaneousNotificationSettings'}} 233 + contentContainerStyle={[a.align_start]}> 234 + <SettingsList.ItemIcon icon={ShapesIcon} /> 235 + <ItemTextWithSubtitle 236 + titleText={<Trans>Everything else</Trans>} 237 + // technically a bundle of several settings, but since they're set together 238 + // and are most likely in sync we'll just show the state of one of them 239 + subtitleText={ 240 + <SettingPreview preference={settings?.starterpackJoined} /> 241 + } 242 + showSkeleton={!settings} 243 + /> 244 + </SettingsList.LinkItem> 245 + </View> 246 + </SettingsList.Container> 247 + </Layout.Content> 248 + </Layout.Screen> 249 + ) 250 + } 251 + 252 + function SettingPreview({ 253 + preference, 254 + }: { 255 + preference?: 256 + | AppBskyNotificationDefs.Preference 257 + | AppBskyNotificationDefs.FilterablePreference 258 + }) { 259 + const {_} = useLingui() 260 + if (!preference) { 261 + return null 262 + } else { 263 + if ('filter' in preference) { 264 + if (preference.filter === 'all') { 265 + if (preference.list && preference.push) { 266 + return _(msg`In-app, Push, Everyone`) 267 + } else if (preference.list) { 268 + return _(msg`In-app, Everyone`) 269 + } else if (preference.push) { 270 + return _(msg`Push, Everyone`) 271 + } 272 + } else if (preference.filter === 'follows') { 273 + if (preference.list && preference.push) { 274 + return _(msg`In-app, Push, People you follow`) 275 + } else if (preference.list) { 276 + return _(msg`In-app, People you follow`) 277 + } else if (preference.push) { 278 + return _(msg`Push, People you follow`) 279 + } 280 + } 281 + } else { 282 + if (preference.list && preference.push) { 283 + return _(msg`In-app, Push`) 284 + } else if (preference.list) { 285 + return _(msg`In-app`) 286 + } else if (preference.push) { 287 + return _(msg`Push`) 288 + } 289 + } 290 + } 291 + 292 + return _(msg`Off`) 293 + }
+9
src/screens/Settings/Settings.tsx
··· 36 36 import {useDialogControl} from '#/components/Dialog' 37 37 import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 38 38 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 39 + import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' 39 40 import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' 40 41 import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' 41 42 import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' ··· 178 179 <SettingsList.ItemIcon icon={HandIcon} /> 179 180 <SettingsList.ItemText> 180 181 <Trans>Moderation</Trans> 182 + </SettingsList.ItemText> 183 + </SettingsList.LinkItem> 184 + <SettingsList.LinkItem 185 + to="/settings/notifications" 186 + label={_(msg`Notifications`)}> 187 + <SettingsList.ItemIcon icon={NotificationIcon} /> 188 + <SettingsList.ItemText> 189 + <Trans>Notifications</Trans> 181 190 </SettingsList.ItemText> 182 191 </SettingsList.LinkItem> 183 192 <SettingsList.LinkItem
+4 -1
src/screens/Settings/SettingsInterests.tsx src/screens/Settings/InterestsSettings.tsx
··· 2 2 import {type TextStyle, View, type ViewStyle} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 6 import {useQueryClient} from '@tanstack/react-query' 6 7 import debounce from 'lodash.debounce' 7 8 9 + import {type CommonNavigatorParams} from '#/lib/routes/types' 8 10 import { 9 11 preferencesQueryKey, 10 12 usePreferencesQuery, ··· 24 26 import {Loader} from '#/components/Loader' 25 27 import {Text} from '#/components/Typography' 26 28 27 - export function SettingsInterests() { 29 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'InterestsSettings'> 30 + export function InterestsSettingsScreen({}: Props) { 28 31 const t = useTheme() 29 32 const gutters = useGutters(['base']) 30 33 const {data: preferences} = usePreferencesQuery()
+45 -54
src/state/queries/notifications/settings.ts
··· 1 - import {msg} from '@lingui/macro' 2 - import {useLingui} from '@lingui/react' 3 - import {useMutation, useQueryClient} from '@tanstack/react-query' 1 + import {type AppBskyNotificationDefs} from '@atproto/api' 2 + import {t} from '@lingui/macro' 3 + import { 4 + type QueryClient, 5 + useMutation, 6 + useQuery, 7 + useQueryClient, 8 + } from '@tanstack/react-query' 4 9 5 - import {until} from '#/lib/async/until' 6 10 import {logger} from '#/logger' 7 - import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 8 - import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' 9 11 import {useAgent} from '#/state/session' 10 12 import * as Toast from '#/view/com/util/Toast' 11 13 12 - export function useNotificationSettingsMutation() { 13 - const {_} = useLingui() 14 + const RQKEY_ROOT = 'notification-settings' 15 + const RQKEY = [RQKEY_ROOT] 16 + 17 + export function useNotificationSettingsQuery() { 18 + const agent = useAgent() 19 + 20 + return useQuery({ 21 + queryKey: RQKEY, 22 + queryFn: async () => { 23 + const response = await agent.app.bsky.notification.getPreferences() 24 + return response.data.preferences 25 + }, 26 + }) 27 + } 28 + export function useNotificationSettingsUpdateMutation() { 14 29 const agent = useAgent() 15 30 const queryClient = useQueryClient() 16 31 17 32 return useMutation({ 18 - mutationFn: async (keys: string[]) => { 19 - const enabled = keys[0] === 'enabled' 20 - 21 - await agent.api.app.bsky.notification.putPreferences({ 22 - priority: enabled, 23 - }) 24 - 25 - await until( 26 - 5, // 5 tries 27 - 1e3, // 1s delay between tries 28 - res => res.data.priority === enabled, 29 - () => agent.api.app.bsky.notification.listNotifications({limit: 1}), 33 + mutationFn: async ( 34 + update: Partial<AppBskyNotificationDefs.Preferences>, 35 + ) => { 36 + const response = await agent.app.bsky.notification.putPreferencesV2( 37 + update, 30 38 ) 31 - 32 - eagerlySetCachedPriority(queryClient, enabled) 39 + return response.data.preferences 33 40 }, 34 - onError: err => { 35 - logger.error('Failed to save notification preferences', { 36 - safeMessage: err, 37 - }) 38 - Toast.show( 39 - _(msg`Failed to save notification preferences, please try again`), 40 - 'xmark', 41 - ) 41 + onMutate: update => { 42 + optimisticUpdateNotificationSettings(queryClient, update) 42 43 }, 43 - onSuccess: () => { 44 - Toast.show(_(msg({message: 'Preference saved', context: 'toast'}))) 45 - }, 46 - onSettled: () => { 47 - invalidateCachedUnreadPage() 48 - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) 49 - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) 44 + onError: e => { 45 + logger.error('Could not update notification settings', {message: e}) 46 + queryClient.invalidateQueries({queryKey: RQKEY}) 47 + Toast.show(t`Could not update notification settings`, 'xmark') 50 48 }, 51 49 }) 52 50 } 53 51 54 - function eagerlySetCachedPriority( 55 - queryClient: ReturnType<typeof useQueryClient>, 56 - enabled: boolean, 52 + function optimisticUpdateNotificationSettings( 53 + queryClient: QueryClient, 54 + update: Partial<AppBskyNotificationDefs.Preferences>, 57 55 ) { 58 - function updateData(old: any) { 59 - if (!old) return old 60 - return { 61 - ...old, 62 - pages: old.pages.map((page: any) => { 63 - return { 64 - ...page, 65 - priority: enabled, 66 - } 67 - }), 68 - } 69 - } 70 - queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) 71 - queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) 56 + queryClient.setQueryData( 57 + RQKEY, 58 + (old?: AppBskyNotificationDefs.Preferences) => { 59 + if (!old) return old 60 + return {...old, ...update} 61 + }, 62 + ) 72 63 }
+1 -1
src/view/screens/Notifications.tsx
··· 130 130 </Layout.Header.Content> 131 131 <Layout.Header.Slot> 132 132 <Link 133 - to="/notifications/settings" 133 + to={{screen: 'NotificationSettings'}} 134 134 label={_(msg`Notification settings`)} 135 135 size="small" 136 136 variant="ghost"
+93 -106
yarn.lock
··· 55 55 resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.2.0.tgz#f39098747dabf8a245d0ed6edc50f362aa4d95f8" 56 56 integrity sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA== 57 57 58 - "@atproto-labs/xrpc-utils@0.0.14": 59 - version "0.0.14" 60 - resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.14.tgz#eefe1ccf61a4288708601324496b0106d5ed4ae3" 61 - integrity sha512-/f0Dhzi08w3Oqv38wdwQ5bw238GbxhYIcxg08kVReEMTlkyRDC6H5RuqHf8Ff9J3FKqjKHGdxaOdrPNM1hCgeQ== 58 + "@atproto-labs/xrpc-utils@0.0.16": 59 + version "0.0.16" 60 + resolved "https://registry.yarnpkg.com/@atproto-labs/xrpc-utils/-/xrpc-utils-0.0.16.tgz#f76c4f615685c60997401f052cbd9f0145d12576" 61 + integrity sha512-WvTQhGjIhFrd/0pMGecE7Xn8BtvvKAgVlNs8UaE6CVRifiCOIvIBwlx1vnslJAavK3FtwL1kKkUdxNtxHciZSQ== 62 62 dependencies: 63 63 "@atproto/xrpc" "^0.7.0" 64 - "@atproto/xrpc-server" "^0.7.18" 64 + "@atproto/xrpc-server" "^0.8.0" 65 65 66 66 "@atproto/api@^0.15.15": 67 67 version "0.15.15" 68 68 resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.15.tgz#a506a1a26f3dfef9adb77234f451c0e784b071e7" 69 69 integrity sha512-Wn8jv76pCvffnkNj68w0CGZ3PT4DJGM8DUZnYq9kEW2im6jbRBYI0yYrHNhSiE92A5Ox0HjL2jMhalsI2p9VlQ== 70 - dependencies: 71 - "@atproto/common-web" "^0.4.2" 72 - "@atproto/lexicon" "^0.4.11" 73 - "@atproto/syntax" "^0.4.0" 74 - "@atproto/xrpc" "^0.7.0" 75 - await-lock "^2.2.2" 76 - multiformats "^9.9.0" 77 - tlds "^1.234.0" 78 - zod "^3.23.8" 79 - 80 - "@atproto/api@^0.15.9": 81 - version "0.15.9" 82 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.15.9.tgz#f8c40afd6e414ab107d63d6f08d9e264bf9a149a" 83 - integrity sha512-CyAILiIcbN+V5CFAI6MDb247epm25RGkP7HSan5LUaOHiyg1NCAmflWCN/bbMdJX9kLqjAPAG3eN4BUUbYe//Q== 84 70 dependencies: 85 71 "@atproto/common-web" "^0.4.2" 86 72 "@atproto/lexicon" "^0.4.11" ··· 108 94 multiformats "^9.9.0" 109 95 uint8arrays "3.0.0" 110 96 111 - "@atproto/bsky@^0.0.151": 112 - version "0.0.151" 113 - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.151.tgz#a0e5b59e163a3b74379fb547601be4fc66b7a133" 114 - integrity sha512-42pvUsyGw0nR6Sxlda824maY4gBxUni1cXPG+7uGe6Ixm6XAaPhfTgT1rAg++1rDXH9tT1EXAVnMxg38S6osLg== 97 + "@atproto/bsky@^0.0.159": 98 + version "0.0.159" 99 + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.159.tgz#6fcfd7d6c73e4041c5abc9ac3b99dbe0e1e6cf76" 100 + integrity sha512-kRjDCW6FbByeafrEoUD5YMhhjuKTvSqrE2/QJ5xe9CP8UIhl8BShm2PcBh9gJtYc7lO83aJPqDSqb5gJwNAJUg== 115 101 dependencies: 116 102 "@atproto-labs/fetch-node" "0.1.9" 117 - "@atproto-labs/xrpc-utils" "0.0.14" 118 - "@atproto/api" "^0.15.9" 103 + "@atproto-labs/xrpc-utils" "0.0.16" 104 + "@atproto/api" "^0.15.15" 119 105 "@atproto/common" "^0.4.11" 120 106 "@atproto/crypto" "^0.4.4" 121 107 "@atproto/did" "^0.1.5" 122 108 "@atproto/identity" "^0.4.8" 123 109 "@atproto/lexicon" "^0.4.11" 124 110 "@atproto/repo" "^0.8.1" 125 - "@atproto/sync" "^0.1.23" 111 + "@atproto/sync" "^0.1.25" 126 112 "@atproto/syntax" "^0.4.0" 127 - "@atproto/xrpc-server" "^0.7.18" 113 + "@atproto/xrpc-server" "^0.8.0" 128 114 "@bufbuild/protobuf" "^1.5.0" 129 115 "@connectrpc/connect" "^1.1.4" 130 116 "@connectrpc/connect-express" "^1.1.4" ··· 154 140 uint8arrays "3.0.0" 155 141 undici "^6.19.8" 156 142 157 - "@atproto/bsync@^0.0.19": 158 - version "0.0.19" 159 - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.19.tgz#bab3d5e4e7c1ca8de16d9b5efebc49dde12d7160" 160 - integrity sha512-AF9aWbU0VlpT//lIuYKhNRplTv+99ld58kfHTS8jfXCpiOZwxwneTkB1hzE+slXJ63K8i/GyzsQCyvRHWzGWCQ== 143 + "@atproto/bsync@^0.0.20": 144 + version "0.0.20" 145 + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.20.tgz#7409c39a1d4715a5be4ce4a545155de238fc9469" 146 + integrity sha512-KbHoZcFpKY869dMQRZXrOXccMkndgLiDY4sfLkCTgp/A0pWw3CKuJmQSmtKHIkWcVkOnfsfJW0J/SgyrXLrn9Q== 161 147 dependencies: 162 148 "@atproto/common" "^0.4.11" 163 149 "@atproto/syntax" "^0.4.0" ··· 232 218 "@noble/hashes" "^1.6.1" 233 219 uint8arrays "3.0.0" 234 220 235 - "@atproto/dev-env@^0.3.133": 236 - version "0.3.133" 237 - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.133.tgz#4ca58c9c4c99f001f26ce50629214f81d6acd3ab" 238 - integrity sha512-GtKDa+q0Fx2tJZL44cDAINMCxNmt1aKkGVpW/6PTnuSSjdA7ErBUEL3opbwgaAcPRGZfscB0mQmGfWR0BUmvUw== 221 + "@atproto/dev-env@^0.3.142": 222 + version "0.3.142" 223 + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.142.tgz#466cff00c92d53ad148709ae50bcca816fcee1bc" 224 + integrity sha512-NiNb3Pdj93goEmKBIF5rIlLSmkfZiwnYmo7U6cGvGXoopRNG5e4Vm2PXb1n7uVdzuvEtiPtpswQxaf0x6jJsWA== 239 225 dependencies: 240 - "@atproto/api" "^0.15.9" 241 - "@atproto/bsky" "^0.0.151" 242 - "@atproto/bsync" "^0.0.19" 226 + "@atproto/api" "^0.15.15" 227 + "@atproto/bsky" "^0.0.159" 228 + "@atproto/bsync" "^0.0.20" 243 229 "@atproto/common-web" "^0.4.2" 244 230 "@atproto/crypto" "^0.4.4" 245 231 "@atproto/identity" "^0.4.8" 246 232 "@atproto/lexicon" "^0.4.11" 247 - "@atproto/ozone" "^0.1.112" 248 - "@atproto/pds" "^0.4.139" 249 - "@atproto/sync" "^0.1.23" 233 + "@atproto/ozone" "^0.1.120" 234 + "@atproto/pds" "^0.4.148" 235 + "@atproto/sync" "^0.1.25" 250 236 "@atproto/syntax" "^0.4.0" 251 - "@atproto/xrpc-server" "^0.7.18" 237 + "@atproto/xrpc-server" "^0.8.0" 252 238 "@did-plc/lib" "^0.0.1" 253 239 "@did-plc/server" "^0.0.1" 254 240 dotenv "^16.0.3" ··· 258 244 uint8arrays "3.0.0" 259 245 undici "^6.14.1" 260 246 261 - "@atproto/did@^0.1.5": 247 + "@atproto/did@0.1.5", "@atproto/did@^0.1.5": 262 248 version "0.1.5" 263 249 resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.1.5.tgz#5bfe73625d54c4c687c00ff370971ce01c39bd61" 264 250 integrity sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ== ··· 273 259 "@atproto/common-web" "^0.4.2" 274 260 "@atproto/crypto" "^0.4.4" 275 261 276 - "@atproto/jwk-jose@0.1.6": 277 - version "0.1.6" 278 - resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.6.tgz#e9fc5a714cd9fe0589f931f3b406585716b5ac03" 279 - integrity sha512-r4DGMvvmazy6CxqAcnplpUxvp6Vd8UwKxQBZRpmm1aNsVonf5qj1yeDkECTiwoe/FPbvtdamlzClB3UZc7Yb5w== 262 + "@atproto/jwk-jose@0.1.8": 263 + version "0.1.8" 264 + resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.8.tgz#2dc8ad2cc900e7bc231add293f6518b06dc017ec" 265 + integrity sha512-aoU2Q0GpIl388KhCcv9YvAxNscALUv3xzLq5gjVPdJ+zmqw94nGZNcjiNvpnbfS+VQM9e2DrrTuwmDXnxfrrSA== 280 266 dependencies: 281 - "@atproto/jwk" "0.1.5" 267 + "@atproto/jwk" "0.3.0" 282 268 jose "^5.2.0" 283 269 284 - "@atproto/jwk@0.1.5": 285 - version "0.1.5" 286 - resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.1.5.tgz#d1e2650431f7f09ed80be48f05908bcd136c2606" 287 - integrity sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg== 270 + "@atproto/jwk@0.3.0": 271 + version "0.3.0" 272 + resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.3.0.tgz#275fa676f6b5988ddedf4ee0475dd285de9b831b" 273 + integrity sha512-MIAXyNMGu1tCNHjqW/8jqfE/wgWCIoK2cJ0mR6UxwhNPvkbe35TcpRYJdtQu/E6MUd7TziyDBa/GO4dKAiePhQ== 288 274 dependencies: 289 275 multiformats "^9.9.0" 290 276 zod "^3.23.8" ··· 300 286 multiformats "^9.9.0" 301 287 zod "^3.23.8" 302 288 303 - "@atproto/oauth-provider-api@0.1.2": 304 - version "0.1.2" 305 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.2.tgz#cdca03af4426f8cf9b09bc9eb57a8604f9513831" 306 - integrity sha512-tNAuMrE6D3696euavxo1+Jh7Re0PPwJstbyY8SrdVPXgKJh/LrbpKUKiPNW/p5KyVfRs2tWeAxy+ReESu6SmXA== 289 + "@atproto/oauth-provider-api@0.1.4": 290 + version "0.1.4" 291 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-api/-/oauth-provider-api-0.1.4.tgz#a775182e3648dc693a04e3cb604eb62cd9ddfd8c" 292 + integrity sha512-3PRrf0gTAVMCETjtIH/3AaQaHBDbjsRBc/OYrlWBZ9IPplchBXtQGH/KcnjE4kK2Ef8p45qQSl3dNWg3EXsbHQ== 307 293 dependencies: 308 - "@atproto/jwk" "0.1.5" 309 - "@atproto/oauth-types" "0.2.7" 294 + "@atproto/jwk" "0.3.0" 295 + "@atproto/oauth-types" "0.3.0" 310 296 311 - "@atproto/oauth-provider-frontend@0.1.5": 312 - version "0.1.5" 313 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.5.tgz#66fd8760fade2ac94111ad5389f33f4d8ce5bba2" 314 - integrity sha512-FdDBuwy827+etjIcRwZU7dtxa8Ltso3ufVLMEi8A2V91v21XDysZjLANC6cvmNNSUcS4E/J6ZAwTrQDo7O5axw== 297 + "@atproto/oauth-provider-frontend@0.1.8": 298 + version "0.1.8" 299 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-frontend/-/oauth-provider-frontend-0.1.8.tgz#21d944566c63f54524f239a10f7c65d150982f40" 300 + integrity sha512-uqfHv+n2xq7vTpuBP1Red7PhpaAbbJbwSbRsSfplJQ16XmF5NCMU8dHGCGRTEHngLZ9UquuIefN3w1QTrNzD0w== 315 301 optionalDependencies: 316 - "@atproto/oauth-provider-api" "0.1.2" 302 + "@atproto/oauth-provider-api" "0.1.4" 317 303 318 - "@atproto/oauth-provider-ui@0.1.6": 319 - version "0.1.6" 320 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.6.tgz#4bae995ff57671ac3915f58fdb2cf6a76a0fe42d" 321 - integrity sha512-pJzV9ouNj1/TDUCl3CWEZrHoUese4lcKx5F59t2OiLFm2K7T7QrszKUIMyU5QdiQHv551B0ZJOkJ8+4b/fVGPA== 304 + "@atproto/oauth-provider-ui@0.1.9": 305 + version "0.1.9" 306 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider-ui/-/oauth-provider-ui-0.1.9.tgz#8c43a1affa94ecb537072e6d569b8a24cdd42e72" 307 + integrity sha512-a6/VAeQWRMxpgnqo/TuqXg3EW2tO68jLh8Mv1uyV1NiZbT7fNlgkII/djIl3fLoEa95I3p236NZxjhKELSBbGg== 322 308 optionalDependencies: 323 - "@atproto/oauth-provider-api" "0.1.2" 309 + "@atproto/oauth-provider-api" "0.1.4" 324 310 325 - "@atproto/oauth-provider@^0.7.8": 326 - version "0.7.8" 327 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.7.8.tgz#287b15eb6b0bc0bb4b2da2339150253db006c6e0" 328 - integrity sha512-+dEU9dTyfWKeZ/Nu7ocR6fO73RcG0vwDjT45vgcnM9L7jtuPk9zfpmiR4ODYBk9QUu2DURo9yBhtXNJI3Yz8aQ== 311 + "@atproto/oauth-provider@^0.9.0": 312 + version "0.9.0" 313 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.9.0.tgz#3598924978c2e3d5fdf62bced54574156d15cf92" 314 + integrity sha512-LbZS9rbR5l9gVO97wJ3ls+ENXwv6BakmArRyjc5EfaQ4Xc3eLbvE629hpu9LV8LyCkBpseum0l+D+rYXsemNUw== 329 315 dependencies: 330 316 "@atproto-labs/fetch" "0.2.3" 331 317 "@atproto-labs/fetch-node" "0.1.9" ··· 333 319 "@atproto-labs/simple-store" "0.2.0" 334 320 "@atproto-labs/simple-store-memory" "0.1.3" 335 321 "@atproto/common" "^0.4.11" 336 - "@atproto/jwk" "0.1.5" 337 - "@atproto/jwk-jose" "0.1.6" 338 - "@atproto/oauth-provider-api" "0.1.2" 339 - "@atproto/oauth-provider-frontend" "0.1.5" 340 - "@atproto/oauth-provider-ui" "0.1.6" 341 - "@atproto/oauth-types" "0.2.7" 322 + "@atproto/did" "0.1.5" 323 + "@atproto/jwk" "0.3.0" 324 + "@atproto/jwk-jose" "0.1.8" 325 + "@atproto/oauth-provider-api" "0.1.4" 326 + "@atproto/oauth-provider-frontend" "0.1.8" 327 + "@atproto/oauth-provider-ui" "0.1.9" 328 + "@atproto/oauth-types" "0.3.0" 342 329 "@atproto/syntax" "0.4.0" 343 330 "@hapi/accept" "^6.0.3" 344 331 "@hapi/address" "^5.1.1" ··· 352 339 jose "^5.2.0" 353 340 zod "^3.23.8" 354 341 355 - "@atproto/oauth-types@0.2.7": 356 - version "0.2.7" 357 - resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.7.tgz#c210868052f8babd98510c19816e3d9a156b33c7" 358 - integrity sha512-2SlDveiSI0oowC+sfuNd/npV8jw/FhokSS26qyUyldTg1g9ZlhxXUfMP4IZOPeZcVn9EszzQRHs1H9ZJqVQIew== 342 + "@atproto/oauth-types@0.3.0": 343 + version "0.3.0" 344 + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.3.0.tgz#8d49d939486ac281bc13d0b1fe4462b7e519fdf0" 345 + integrity sha512-ptfsJARKODXfuOoDQag4a6PpEkDEj4Urz3jOmnQZy2YspPc/TNm1o0HglU0YehELv1vfhh9gEz40BJztPPhiLA== 359 346 dependencies: 360 - "@atproto/jwk" "0.1.5" 347 + "@atproto/jwk" "0.3.0" 361 348 zod "^3.23.8" 362 349 363 - "@atproto/ozone@^0.1.112": 364 - version "0.1.112" 365 - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.112.tgz#6b6b5ac052dd4e6dfec3c88f83c9b53f4902fcbe" 366 - integrity sha512-Euut64N/4UyRXyV6m1ATE9K6o6EpCf46ozD4GG8HJ9AC5zEgBYMSkH4l6SLrhKrYYIGXkvglk1WYuuDQKYb3LA== 350 + "@atproto/ozone@^0.1.120": 351 + version "0.1.120" 352 + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.120.tgz#b5c431a538558179de0465cfbfea512d65e092bf" 353 + integrity sha512-zu2f16K/z/3r4mC4z/8qISPt0j+Y0GwtjmSE+VOJvVT363iOd9a834K+QHJqnD6B3iTBHR1VPlZ/4fsZ3+4UaA== 367 354 dependencies: 368 - "@atproto/api" "^0.15.9" 355 + "@atproto/api" "^0.15.15" 369 356 "@atproto/common" "^0.4.11" 370 357 "@atproto/crypto" "^0.4.4" 371 358 "@atproto/identity" "^0.4.8" 372 359 "@atproto/lexicon" "^0.4.11" 373 360 "@atproto/syntax" "^0.4.0" 374 361 "@atproto/xrpc" "^0.7.0" 375 - "@atproto/xrpc-server" "^0.7.18" 362 + "@atproto/xrpc-server" "^0.8.0" 376 363 "@did-plc/lib" "^0.0.1" 377 364 compression "^1.7.4" 378 365 cors "^2.8.5" ··· 390 377 undici "^6.14.1" 391 378 ws "^8.12.0" 392 379 393 - "@atproto/pds@^0.4.139": 394 - version "0.4.139" 395 - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.139.tgz#70ae5afd7d90eab214c652d57a5e6478af454fbe" 396 - integrity sha512-VD1VTSAnbAme4D4Xk/Wdl05qs8YbCe39/i960EyXzw2fYNvL9jMpKm3z0lwhrYN9q7phFhr2ubU2QjfRFDbDAQ== 380 + "@atproto/pds@^0.4.148": 381 + version "0.4.148" 382 + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.148.tgz#7af82480e42174ea1c284f5975bf0aff3bd4da24" 383 + integrity sha512-PbxTpxRAcsdu3zANjwNH+Pfbu0pfj5z6UDmcnc3eaEP11xsExu3+B84jOYKAkIN/PbM1A9EbiBjKb95yBQxGAw== 397 384 dependencies: 398 385 "@atproto-labs/fetch-node" "0.1.9" 399 - "@atproto-labs/xrpc-utils" "0.0.14" 400 - "@atproto/api" "^0.15.9" 386 + "@atproto-labs/xrpc-utils" "0.0.16" 387 + "@atproto/api" "^0.15.15" 401 388 "@atproto/aws" "^0.2.21" 402 389 "@atproto/common" "^0.4.11" 403 390 "@atproto/crypto" "^0.4.4" 404 391 "@atproto/identity" "^0.4.8" 405 392 "@atproto/lexicon" "^0.4.11" 406 - "@atproto/oauth-provider" "^0.7.8" 393 + "@atproto/oauth-provider" "^0.9.0" 407 394 "@atproto/repo" "^0.8.1" 408 395 "@atproto/syntax" "^0.4.0" 409 396 "@atproto/xrpc" "^0.7.0" 410 - "@atproto/xrpc-server" "^0.7.18" 397 + "@atproto/xrpc-server" "^0.8.0" 411 398 "@did-plc/lib" "^0.0.4" 412 399 "@hapi/address" "^5.1.1" 413 400 better-sqlite3 "^10.0.0" ··· 452 439 varint "^6.0.0" 453 440 zod "^3.23.8" 454 441 455 - "@atproto/sync@^0.1.23": 456 - version "0.1.23" 457 - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.23.tgz#01d4ecf9d5ddc624d14e8fb98927f0b2a97eafeb" 458 - integrity sha512-1ItRNHMLMcBeTziOZpxS4Q+ha2enQce3fSiAQaCpLCQ8VTNq1D1aRR6ePZCQFzab9jDDtBz0v4FufOnMByRIeg== 442 + "@atproto/sync@^0.1.25": 443 + version "0.1.25" 444 + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.25.tgz#66b3453e3cf0ba6a155dbbf207ea46d632c2d6b0" 445 + integrity sha512-4UsQgQsUK+hKFAEDi10Ops6n2W/kfk2JYP8AU6FSHAzOadB1hKRDJbGF5vLiLP9ACBhCzoJerZ31DCnhjzRzfw== 459 446 dependencies: 460 447 "@atproto/common" "^0.4.11" 461 448 "@atproto/identity" "^0.4.8" 462 449 "@atproto/lexicon" "^0.4.11" 463 450 "@atproto/repo" "^0.8.1" 464 451 "@atproto/syntax" "^0.4.0" 465 - "@atproto/xrpc-server" "^0.7.18" 452 + "@atproto/xrpc-server" "^0.8.0" 466 453 multiformats "^9.9.0" 467 454 p-queue "^6.6.2" 468 455 ws "^8.12.0" ··· 472 459 resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.4.0.tgz#bec71552087bb24c208a06ef418c0040b65542f2" 473 460 integrity sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA== 474 461 475 - "@atproto/xrpc-server@^0.7.18": 476 - version "0.7.18" 477 - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.18.tgz#7cb6e517da2afec1c9bee70d92c07667a80718ec" 478 - integrity sha512-kjlAsI+UNbbm6AK3Y5Hb4BJ7VQHNKiYYu2kX5vhZJZHO8qfO40GPYYb/2TknZV8IG6fDPBQhUpcDRolI86sgag== 462 + "@atproto/xrpc-server@^0.8.0": 463 + version "0.8.0" 464 + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.8.0.tgz#a32c9c71411ec6ee476fcd0260d5e9e80be348bd" 465 + integrity sha512-jDAEVHVhM4IvC0y491gXBuD4b1D9/XrM3HaEronRneAdNZ0qE0nsiJNqiHfQ6r4BvFdHnABM9KyHV9EQTvmxfg== 479 466 dependencies: 480 467 "@atproto/common" "^0.4.11" 481 468 "@atproto/crypto" "^0.4.4"