Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add pet account label functionality & settings

also a quiet failure for state/session/util? surely that won't be a problem? :3

xan.lol 5c2c9b2e 43c9c9b2

+444 -9
+9
src/Navigation.tsx
··· 124 124 import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings' 125 125 import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings' 126 126 import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' 127 + import {PetLabelSettingsScreen} from '#/screens/Settings/PetLabelSettings' 127 128 import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 128 129 import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings' 129 130 import {SettingsScreen} from '#/screens/Settings/Settings' ··· 443 444 getComponent={() => AutomationLabelSettingsScreen} 444 445 options={{ 445 446 title: title(msg`Automation Label`), 447 + requireAuth: true, 448 + }} 449 + /> 450 + <Stack.Screen 451 + name="PetLabelSettings" 452 + getComponent={() => PetLabelSettingsScreen} 453 + options={{ 454 + title: title(msg`Pet Label`), 446 455 requireAuth: true, 447 456 }} 448 457 />
+2
src/analytics/metrics/types.ts
··· 842 842 843 843 'bot:label:toggle': {state: 'add' | 'remove'} 844 844 'bot:badge:click': {} 845 + 'pet:label:toggle': {state: 'add' | 'remove'} 846 + 'pet:badge:click': {} 845 847 846 848 'live:create': {duration: number} 847 849 'live:edit': {}
+79
src/components/PetAccountAlert.tsx
··· 1 + import {View} from 'react-native' 2 + import {Trans, useLingui} from '@lingui/react/macro' 3 + 4 + import {useSession} from '#/state/session' 5 + import {atoms as a, useTheme, web} from '#/alf' 6 + import {Button, ButtonText} from '#/components/Button' 7 + import * as Dialog from '#/components/Dialog' 8 + import {Pet_Filled as PetIcon} from '#/components/icons/Pet' 9 + import {Text} from '#/components/Typography' 10 + import {navigate} from '#/Navigation' 11 + import type * as bsky from '#/types/bsky' 12 + 13 + export function PetAccountAlert({ 14 + control, 15 + profile, 16 + }: { 17 + control: Dialog.DialogControlProps 18 + profile: bsky.profile.AnyProfileView 19 + }) { 20 + const {t: l} = useLingui() 21 + const t = useTheme() 22 + const {currentAccount} = useSession() 23 + 24 + const isSelf = profile.did === currentAccount?.did 25 + const description = isSelf 26 + ? l`You have marked this account as a pet account. You can remove it at any time from your account settings.` 27 + : l`This account has been marked as a pet account by its owner.` 28 + 29 + return ( 30 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 31 + <Dialog.ScrollableInner 32 + label={l`Pet account`} 33 + style={[web({maxWidth: 320})]}> 34 + <View style={[a.align_center, a.pb_md, a.shadow_sm]}> 35 + <PetIcon width={48} fill={t.atoms.text_contrast_medium.color} /> 36 + </View> 37 + <View style={[a.align_center]}> 38 + <Text 39 + style={[ 40 + a.leading_snug, 41 + a.text_center, 42 + a.pb_xl, 43 + a.text_md, 44 + t.atoms.text_contrast_high, 45 + {maxWidth: 300}, 46 + ]}> 47 + {description} 48 + </Text> 49 + </View> 50 + <View style={[a.w_full, a.gap_sm]}> 51 + <Button 52 + label={l`Okay`} 53 + onPress={() => control.close()} 54 + color="primary" 55 + size="large"> 56 + <ButtonText> 57 + <Trans>Okay</Trans> 58 + </ButtonText> 59 + </Button> 60 + {isSelf ? ( 61 + <Button 62 + label={l`Open settings`} 63 + onPress={() => { 64 + control.close(() => { 65 + navigate('PetLabelSettings') 66 + }) 67 + }} 68 + color="secondary" 69 + size="large"> 70 + <ButtonText> 71 + <Trans>Open settings</Trans> 72 + </ButtonText> 73 + </Button> 74 + ) : null} 75 + </View> 76 + </Dialog.ScrollableInner> 77 + </Dialog.Outer> 78 + ) 79 + }
+89
src/components/PetBadge.tsx
··· 1 + import {View} from 'react-native' 2 + import {type ComAtprotoLabelDefs} from '@atproto/api' 3 + import {useLingui} from '@lingui/react/macro' 4 + 5 + import {atoms as a, useTheme} from '#/alf' 6 + import {Button} from '#/components/Button' 7 + import {useDialogControl} from '#/components/Dialog' 8 + import {Pet_Filled as PetIcon} from '#/components/icons/Pet' 9 + import {PetAccountAlert} from '#/components/PetAccountAlert' 10 + import {useAnalytics} from '#/analytics' 11 + import type * as bsky from '#/types/bsky' 12 + 13 + export function isPetAccount(profile: { 14 + did: string 15 + labels?: ComAtprotoLabelDefs.Label[] 16 + }): boolean { 17 + return ( 18 + profile.labels?.some(l => l.val === 'pet' && l.src === profile.did) ?? false 19 + ) 20 + } 21 + 22 + export function PetBadge({ 23 + profile, 24 + alwaysShow = false, 25 + width, 26 + }: { 27 + profile: bsky.profile.AnyProfileView 28 + alwaysShow?: boolean 29 + width: number 30 + }) { 31 + const t = useTheme() 32 + 33 + if (!isPetAccount(profile) && !alwaysShow) { 34 + return null 35 + } 36 + 37 + return ( 38 + <View> 39 + <PetIcon width={width} fill={t.atoms.text_contrast_medium.color} /> 40 + </View> 41 + ) 42 + } 43 + 44 + export function PetBadgeButton({ 45 + profile, 46 + width, 47 + }: { 48 + profile: bsky.profile.AnyProfileView 49 + width: number 50 + }) { 51 + const t = useTheme() 52 + const ax = useAnalytics() 53 + const {t: l} = useLingui() 54 + const control = useDialogControl() 55 + 56 + if (!isPetAccount(profile)) { 57 + return null 58 + } 59 + 60 + return ( 61 + <> 62 + <Button 63 + label={l`Pet account`} 64 + hitSlop={20} 65 + onPress={evt => { 66 + evt.preventDefault() 67 + ax.metric('pet:badge:click', {}) 68 + control.open() 69 + }}> 70 + {({hovered}) => ( 71 + <View 72 + style={[ 73 + a.justify_end, 74 + a.align_end, 75 + a.transition_transform, 76 + { 77 + width: width, 78 + height: width, 79 + transform: [{scale: hovered ? 1.1 : 1}], 80 + }, 81 + ]}> 82 + <PetIcon width={width} fill={t.atoms.text_contrast_medium.color} /> 83 + </View> 84 + )} 85 + </Button> 86 + <PetAccountAlert control={control} profile={profile} /> 87 + </> 88 + ) 89 + }
+9 -1
src/components/ProfileBadges.tsx
··· 3 3 import {useProfileShadow} from '#/state/cache/profile-shadow' 4 4 import {atoms as a, useAlf, type ViewStyleProp} from '#/alf' 5 5 import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge' 6 + import {isPetAccount, PetBadge, PetBadgeButton} from '#/components/PetBadge' 6 7 import {useSimpleVerificationState} from '#/components/verification' 7 8 import {VerificationCheck} from '#/components/verification/VerificationCheck' 8 9 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' ··· 44 45 } = useAlf() 45 46 46 47 // if nothing to show, don't render the container at all 47 - if (!verification.showBadge && !isBotAccount(shadowed)) return null 48 + if ( 49 + !verification.showBadge && 50 + !isBotAccount(shadowed) && 51 + !isPetAccount(shadowed) 52 + ) 53 + return null 48 54 49 55 const isOnTheSmallSide = size === 'xs' || size === 'sm' 50 56 ··· 68 74 width={verificationIconWidth} 69 75 /> 70 76 <BotBadgeButton profile={shadowed} width={botIconWidth} /> 77 + <PetBadgeButton profile={shadowed} width={botIconWidth} /> 71 78 </> 72 79 ) : ( 73 80 <> ··· 78 85 /> 79 86 )} 80 87 <BotBadge profile={shadowed} width={botIconWidth} /> 88 + <PetBadge profile={shadowed} width={botIconWidth} /> 81 89 </> 82 90 )} 83 91 </View>
+9
src/components/icons/Pet.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Pet_Stroke = createSinglePathSVG({ 4 + path: 'M12.1749 14.3884C12.6123 14.3884 13.0065 14.6523 13.174 15.0564C13.3412 15.4604 13.2488 15.9258 12.9396 16.2351L12.129 17.0466C11.7067 17.4686 11.0211 17.4687 10.5988 17.0466L9.78823 16.2351C9.47903 15.9259 9.38576 15.4604 9.55288 15.0564C9.72026 14.6523 10.1155 14.3885 10.5529 14.3884H12.1749ZM7.03725 11.9548C7.6346 11.9548 8.11928 12.4395 8.11928 13.0368V13.5778C8.1189 14.1749 7.63437 14.6589 7.03725 14.6589C6.44025 14.6588 5.9556 14.1748 5.95522 13.5778V13.0368C5.95522 12.4396 6.44001 11.9549 7.03725 11.9548ZM15.6906 11.9548C16.2877 11.955 16.7716 12.4396 16.7716 13.0368V13.5778C16.7712 14.1747 16.2875 14.6587 15.6906 14.6589C15.0936 14.6588 14.6089 14.1748 14.6085 13.5778V13.0368C14.6085 12.4396 15.0933 11.9549 15.6906 11.9548ZM5.23647 0.0729559C6.53373 -0.143592 7.79123 0.134641 8.74721 0.738972C9.34249 1.11535 9.86235 1.65154 10.1652 2.30342C11.0212 2.19123 11.8888 2.20094 12.7423 2.33077C13.0632 1.67007 13.6104 1.13198 14.213 0.757526C15.1887 0.151552 16.4731 -0.146061 17.7794 0.0719794L17.9826 0.112018C18.9872 0.338629 19.7886 0.966086 20.4025 1.68038C21.0591 2.44451 21.5693 3.38404 21.9523 4.29171C22.3379 5.20561 22.6146 6.13363 22.7882 6.90889C22.9501 7.632 23.055 8.36541 23.0021 8.83175C22.9394 9.38051 22.6111 9.81363 22.3058 10.0993C21.9779 10.4061 21.5566 10.6604 21.0861 10.8396C20.9985 10.8729 20.9079 10.9019 20.8156 10.9304C20.9978 11.8188 21.0937 12.7237 21.0978 13.6325V13.6374C21.0978 16.2427 19.9628 18.3023 18.133 19.6794C16.3342 21.033 13.9234 21.6892 11.3634 21.6892C8.80365 21.6891 6.39348 21.0329 4.59487 19.6794C2.87936 18.3884 1.77422 16.4978 1.64272 14.1198L1.62905 13.6374C1.62869 12.6955 1.7251 11.7491 1.89956 10.8269C1.44131 10.6483 1.03091 10.3996 0.710104 10.0993C0.404842 9.81361 0.0764914 9.38152 0.0138148 8.83272C-0.0390767 8.36641 0.0658183 7.63194 0.227682 6.90889C0.401283 6.1336 0.678972 5.20564 1.0646 4.29171C1.44758 3.38413 1.95688 2.44446 2.61342 1.68038C3.26799 0.918697 4.13642 0.25648 5.23647 0.0729559ZM5.88296 2.17061L5.59292 2.20577C5.16701 2.27674 4.71563 2.55254 4.25405 3.08956C3.79427 3.62466 3.38988 4.34548 3.05776 5.13253C2.72835 5.9132 2.48801 6.71612 2.33901 7.38155C2.26469 7.71353 2.21493 8.00202 2.18764 8.22725C2.17439 8.3368 2.16715 8.42559 2.16421 8.49288C2.17154 8.50045 2.17871 8.51095 2.18862 8.52022C2.28391 8.6093 2.45468 8.7245 2.70034 8.81807C3.12331 8.97909 3.7084 9.05259 4.37417 8.87178L4.66421 8.77803C5.64491 8.41072 6.21922 7.65525 6.56264 6.71651C6.76778 6.15558 7.38841 5.86698 7.94936 6.07198C8.51026 6.27711 8.79882 6.89778 8.59389 7.4587C8.13716 8.70742 7.28398 9.98066 5.74038 10.6735L5.42202 10.8044C4.95124 10.9804 4.48856 11.0792 4.04506 11.1189C3.88082 11.9532 3.79275 12.8032 3.79311 13.6374L3.8019 13.9899C3.89478 15.7215 4.67521 17.0324 5.89565 17.9509C7.22862 18.954 9.14506 19.526 11.3634 19.5261C13.582 19.5261 15.4991 18.9541 16.8322 17.9509C18.133 16.9719 18.9334 15.5471 18.9347 13.6423C18.9309 12.7778 18.829 11.9175 18.6339 11.0769C18.2954 11.0235 17.9473 10.9362 17.5949 10.8044C15.8057 10.1345 15.0009 8.72918 14.5363 7.4587C14.3314 6.89795 14.6193 6.27735 15.1798 6.07198C15.7408 5.86681 16.3624 6.1555 16.5675 6.71651C16.9332 7.7163 17.4139 8.42668 18.3507 8.77803C19.1362 9.07172 19.8331 9.00213 20.3165 8.81807C20.5623 8.72447 20.733 8.6093 20.8283 8.52022C20.8376 8.51149 20.8437 8.50111 20.8507 8.49385C20.8478 8.42644 20.8416 8.33733 20.8283 8.22725C20.801 8.00199 20.7512 7.71359 20.6769 7.38155C20.5279 6.71614 20.2885 5.91316 19.9591 5.13253C19.627 4.34541 19.2217 3.62467 18.7619 3.08956C18.3004 2.5527 17.8498 2.27675 17.424 2.20577C16.6506 2.07675 15.8961 2.25911 15.3546 2.59542C14.7919 2.94512 14.6087 3.34816 14.6085 3.60128C14.6085 3.93905 14.4506 4.25812 14.1818 4.4626C13.913 4.66691 13.5638 4.73429 13.2384 4.64425C12.0597 4.31749 10.8156 4.30326 9.63003 4.60421C9.30662 4.68624 8.96274 4.61472 8.69936 4.40987C8.43622 4.20504 8.28251 3.88982 8.28237 3.55635C8.28237 3.27806 8.1024 2.89145 7.59096 2.56807C7.15478 2.29233 6.5513 2.12136 5.88296 2.17061Z', 5 + }) 6 + 7 + export const Pet_Filled = createSinglePathSVG({ 8 + path: 'M5.4607 0.0759783C6.8135 -0.149823 8.12498 0.141011 9.12183 0.771291C9.74266 1.16387 10.2846 1.72317 10.6003 2.40313C11.4931 2.28617 12.3977 2.29597 13.2878 2.43145C13.6226 1.74237 14.1924 1.18128 14.821 0.790822C15.8384 0.15895 17.1776 -0.151409 18.5398 0.0759783L18.5388 0.0769548C19.6864 0.268076 20.5924 0.95827 21.2751 1.75274C21.9598 2.5495 22.491 3.52898 22.8904 4.47539C23.2926 5.42852 23.5814 6.39634 23.7625 7.20488C23.9313 7.95914 24.0404 8.72448 23.9851 9.21074C23.8846 10.0928 23.2137 10.6255 22.6726 10.9236C22.3819 11.0838 22.0502 11.2201 21.6921 11.3387C21.8906 12.2842 21.9955 13.2484 21.9998 14.2166V14.2215C21.9997 16.9378 20.8157 19.0844 18.908 20.5203C17.0324 21.9318 14.5187 22.617 11.8494 22.617C9.18018 22.617 6.66725 21.9317 4.79175 20.5203C3.00314 19.1742 1.84989 17.2037 1.71265 14.7244L1.69898 14.2215C1.69856 13.2066 1.84375 12.0447 2.05249 10.991C1.77366 10.9 1.51629 10.803 1.2898 10.6951C1.0423 10.5772 0.778789 10.4242 0.556399 10.2205C0.337816 10.0203 0.0686425 9.68644 0.0144069 9.21074C-0.0408745 8.72446 0.0691656 7.95916 0.23804 7.20488C0.41909 6.39632 0.707901 5.42854 1.11011 4.47539C1.50951 3.52894 2.04063 2.54951 2.72534 1.75274C3.40798 0.958399 4.3134 0.267218 5.4607 0.0759783ZM10.7244 14.9109C10.1178 14.911 9.57081 15.2763 9.33863 15.8367C9.10655 16.3972 9.23489 17.0425 9.66382 17.4715L10.7888 18.5965C11.0701 18.8777 11.4516 19.0359 11.8494 19.0359C12.2471 19.0359 12.6286 18.8777 12.9099 18.5965L14.0349 17.4715C14.4639 17.0425 14.5922 16.3972 14.3601 15.8367C14.1279 15.2763 13.581 14.9109 12.9744 14.9109H10.7244ZM5.84937 11.5359C5.02096 11.5359 4.34937 12.2075 4.34937 13.0359V13.7859C4.34947 14.6143 5.02102 15.2859 5.84937 15.2859C6.67772 15.2859 7.34927 14.6143 7.34937 13.7859V13.0359C7.34937 12.2075 6.67778 11.5359 5.84937 11.5359ZM17.8494 11.5359C17.021 11.5359 16.3494 12.2075 16.3494 13.0359V13.7859C16.3495 14.6143 17.021 15.2859 17.8494 15.2859C18.6777 15.2859 19.3493 14.6143 19.3494 13.7859V13.0359C19.3494 12.2075 18.6778 11.5359 17.8494 11.5359Z', 9 + })
+1
src/lib/routes/types.ts
··· 53 53 RunesSettings: undefined 54 54 AccountSettings: undefined 55 55 AutomationLabelSettings: undefined 56 + PetLabelSettings: undefined 56 57 PrivacyAndSecuritySettings: undefined 57 58 ActivityPrivacySettings: undefined 58 59 ContentAndMediaSettings: undefined
+1
src/routes.ts
··· 53 53 SavedFeeds: '/settings/saved-feeds', 54 54 AccountSettings: '/settings/account', 55 55 AutomationLabelSettings: '/settings/automation-label', 56 + PetLabelSettings: '/settings/pet-label', 56 57 PrivacyAndSecuritySettings: '/settings/privacy-and-security', 57 58 ActivityPrivacySettings: '/settings/privacy-and-security/activity', 58 59 ContentAndMediaSettings: '/settings/content-and-media',
+15
src/screens/Settings/AccountSettings.tsx
··· 24 24 import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze' 25 25 import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' 26 26 import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/icons/Pencil' 27 + import {Pet_Stroke as PetIcon} from '#/components/icons/Pet' 27 28 import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield' 28 29 import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash' 29 30 import * as Layout from '#/components/Layout' 31 + import {isPetAccount} from '#/components/PetBadge' 30 32 import {ChangeHandleDialog} from './components/ChangeHandleDialog' 31 33 import {ChangePasswordDialog} from './components/ChangePasswordDialog' 32 34 import {DeactivateAccountDialog} from './components/DeactivateAccountDialog' ··· 162 164 {profile && ( 163 165 <SettingsList.BadgeText> 164 166 {isBotAccount(profile) ? _(msg`On`) : _(msg`Off`)} 167 + </SettingsList.BadgeText> 168 + )} 169 + </SettingsList.LinkItem> 170 + <SettingsList.LinkItem 171 + to="/settings/pet-label" 172 + label={_(msg`Pet label`)}> 173 + <SettingsList.ItemIcon icon={PetIcon} /> 174 + <SettingsList.ItemText> 175 + <Trans>Pet label</Trans> 176 + </SettingsList.ItemText> 177 + {profile && ( 178 + <SettingsList.BadgeText> 179 + {isPetAccount(profile) ? _(msg`On`) : _(msg`Off`)} 165 180 </SettingsList.BadgeText> 166 181 )} 167 182 </SettingsList.LinkItem>
+199
src/screens/Settings/PetLabelSettings.tsx
··· 1 + import {View} from 'react-native' 2 + import {type $Typed, ComAtprotoLabelDefs} from '@atproto/api' 3 + import {Trans, useLingui} from '@lingui/react/macro' 4 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 + import {useQueryClient} from '@tanstack/react-query' 6 + 7 + import {type CommonNavigatorParams} from '#/lib/routes/types' 8 + import {RQKEY_ROOT as POST_FEED_RQKEY_ROOT} from '#/state/queries/post-feed' 9 + import { 10 + useProfileQuery, 11 + useProfileUpdateMutation, 12 + } from '#/state/queries/profile' 13 + import {postThreadQueryKeyRoot} from '#/state/queries/usePostThread/types' 14 + import {useSession} from '#/state/session' 15 + import {UserAvatar} from '#/view/com/util/UserAvatar' 16 + import {atoms as a, platform, useTheme} from '#/alf' 17 + import * as Toggle from '#/components/forms/Toggle' 18 + import {Pet_Filled as PetIcon} from '#/components/icons/Pet' 19 + import * as Layout from '#/components/Layout' 20 + import {PetBadge} from '#/components/PetBadge' 21 + import {Text} from '#/components/Typography' 22 + import {useSimpleVerificationState} from '#/components/verification' 23 + import {VerificationCheck} from '#/components/verification/VerificationCheck' 24 + import {useAnalytics} from '#/analytics' 25 + import * as bsky from '#/types/bsky' 26 + 27 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'PetLabelSettings'> 28 + export function PetLabelSettingsScreen({}: Props) { 29 + const t = useTheme() 30 + const ax = useAnalytics() 31 + const {t: l} = useLingui() 32 + const queryClient = useQueryClient() 33 + const {currentAccount} = useSession() 34 + const {data: profile} = useProfileQuery({did: currentAccount?.did}) 35 + const updateProfile = useProfileUpdateMutation() 36 + const verification = useSimpleVerificationState({profile}) 37 + 38 + const isPetLabeled = 39 + profile?.labels?.some(l => l.val === 'pet' && l.src === profile.did) ?? 40 + false 41 + const canToggle = profile && !updateProfile.isPending 42 + 43 + const onToggle = () => { 44 + if (!profile) { 45 + return 46 + } 47 + let wasAdded = false 48 + ax.metric('pet:label:toggle', {state: isPetLabeled ? 'remove' : 'add'}) 49 + updateProfile.mutate( 50 + { 51 + profile, 52 + updates: existing => { 53 + const labels: $Typed<ComAtprotoLabelDefs.SelfLabels> = bsky.validate( 54 + existing.labels, 55 + ComAtprotoLabelDefs.validateSelfLabels, 56 + ) 57 + ? existing.labels 58 + : { 59 + $type: 'com.atproto.label.defs#selfLabels', 60 + values: [], 61 + } 62 + 63 + const hasLabel = labels.values.some(l => l.val === 'pet') 64 + if (hasLabel) { 65 + wasAdded = false 66 + labels.values = labels.values.filter(l => l.val !== 'pet') 67 + } else { 68 + wasAdded = true 69 + labels.values.push({val: 'pet'}) 70 + } 71 + 72 + if (labels.values.length === 0) { 73 + delete existing.labels 74 + } else { 75 + existing.labels = labels 76 + } 77 + 78 + return existing 79 + }, 80 + checkCommitted: res => { 81 + const exists = !!res.data.labels?.some(l => l.val === 'pet') 82 + return exists === wasAdded 83 + }, 84 + }, 85 + { 86 + onSuccess() { 87 + queryClient.invalidateQueries({queryKey: [POST_FEED_RQKEY_ROOT]}) 88 + queryClient.invalidateQueries({queryKey: [postThreadQueryKeyRoot]}) 89 + }, 90 + }, 91 + ) 92 + } 93 + 94 + return ( 95 + <Layout.Screen> 96 + <Layout.Header.Outer> 97 + <Layout.Header.BackButton /> 98 + <Layout.Header.Content> 99 + <Layout.Header.TitleText> 100 + <Trans>Pet Label</Trans> 101 + </Layout.Header.TitleText> 102 + </Layout.Header.Content> 103 + <Layout.Header.Slot /> 104 + </Layout.Header.Outer> 105 + <Layout.Content> 106 + <View style={[a.p_xl, a.gap_xl]}> 107 + {profile && ( 108 + <View 109 + style={[ 110 + a.flex_row, 111 + a.justify_center, 112 + a.align_center, 113 + a.gap_sm, 114 + a.rounded_lg, 115 + a.border, 116 + t.atoms.bg_contrast_50, 117 + t.atoms.border_contrast_low, 118 + { 119 + height: 160, 120 + paddingRight: 20, 121 + }, 122 + ]}> 123 + <UserAvatar size={42} avatar={profile.avatar} type="user" /> 124 + <View> 125 + <View style={[a.flex_row, a.align_baseline]}> 126 + <View style={[a.flex_row, a.align_center, a.gap_xs]}> 127 + <Text 128 + emoji 129 + style={[ 130 + a.text_xl, 131 + a.font_semi_bold, 132 + a.flex_shrink, 133 + a.leading_tight, 134 + ]} 135 + numberOfLines={1}> 136 + {profile.displayName || profile.handle} 137 + </Text> 138 + {verification.isVerified && ( 139 + <VerificationCheck 140 + verifier={verification.role === 'verifier'} 141 + size="sm" 142 + /> 143 + )} 144 + <View style={{top: platform({ios: -1})}}> 145 + <PetBadge profile={profile} alwaysShow width={17} /> 146 + </View> 147 + </View> 148 + </View> 149 + <Text 150 + style={[ 151 + a.text_md, 152 + a.leading_snug, 153 + t.atoms.text_contrast_medium, 154 + ]} 155 + numberOfLines={1}> 156 + @{profile.handle} 157 + </Text> 158 + </View> 159 + </View> 160 + )} 161 + <View style={[a.gap_sm]}> 162 + <Text style={[a.text_2xl, a.font_bold]}> 163 + <Trans>Add pet label to account</Trans> 164 + </Text> 165 + <Text style={[a.text_md, a.leading_snug]}> 166 + <Trans> 167 + This label lets the world know that the account holder is a pet. 168 + If turned on, this label appears next to the account's name on 169 + their profile and posts. It can be turned on or off at any time. 170 + </Trans> 171 + </Text> 172 + </View> 173 + <Toggle.Item 174 + name="pet_label" 175 + disabled={!canToggle || updateProfile.isPending} 176 + value={isPetLabeled} 177 + onChange={onToggle} 178 + label={l`Show pet label`} 179 + style={[ 180 + a.w_full, 181 + a.p_md, 182 + a.rounded_lg, 183 + a.border, 184 + t.atoms.border_contrast_low, 185 + t.atoms.bg_contrast_50, 186 + ]}> 187 + <View style={[a.pr_xs]}> 188 + <PetIcon width={24} fill={t.atoms.text_contrast_medium.color} /> 189 + </View> 190 + <Toggle.LabelText style={[a.flex_1, a.text_md, a.font_medium]}> 191 + <Trans>Show pet label</Trans> 192 + </Toggle.LabelText> 193 + <Toggle.Platform /> 194 + </Toggle.Item> 195 + </View> 196 + </Layout.Content> 197 + </Layout.Screen> 198 + ) 199 + }
+22 -3
src/state/session/__tests__/session-test.ts
··· 1 1 import {BskyAgent} from '@atproto/api' 2 2 import {describe, expect, it, jest} from '@jest/globals' 3 + import {jwtDecode} from 'jwt-decode' 3 4 4 5 import {agentToSessionAccountOrThrow} from '../agent' 5 6 import {type Action, getInitialState, reducer, type State} from '../reducer' 6 7 7 8 jest.mock('jwt-decode', () => ({ 8 - jwtDecode(_token: string) { 9 - return {} 10 - }, 9 + jwtDecode: jest.fn(() => ({})), 11 10 })) 11 + 12 + const mockedJwtDecode = jest.mocked(jwtDecode) 12 13 13 14 jest.mock('../../birthdate') 14 15 jest.mock('../../../ageAssurance/data') ··· 19 20 })) 20 21 21 22 describe('session', () => { 23 + it('does not throw when accessJwt is malformed', () => { 24 + mockedJwtDecode.mockImplementationOnce(() => { 25 + throw new Error('Invalid token specified: missing part #2') 26 + }) 27 + 28 + const agent = new BskyAgent({service: 'https://alice.com'}) 29 + agent.sessionManager.session = { 30 + active: true, 31 + did: 'alice-did', 32 + handle: 'alice.test', 33 + accessJwt: 'invalid-token', 34 + refreshJwt: 'alice-refresh-jwt-1', 35 + } 36 + 37 + expect(() => agentToSessionAccountOrThrow(agent)).not.toThrow() 38 + expect(agentToSessionAccountOrThrow(agent).signupQueued).toBe(false) 39 + }) 40 + 22 41 it('can log in and out', () => { 23 42 let state = getInitialState([]) 24 43 expect(printState(state)).toMatchInlineSnapshot(`
+9 -5
src/state/session/util.ts
··· 14 14 15 15 export function isSignupQueued(accessJwt: string | undefined) { 16 16 if (accessJwt) { 17 - const sessData = jwtDecode(accessJwt) 18 - return ( 19 - hasProp(sessData, 'scope') && 20 - sessData.scope === 'com.atproto.signupQueued' 21 - ) 17 + try { 18 + const sessData = jwtDecode(accessJwt) 19 + return ( 20 + hasProp(sessData, 'scope') && 21 + sessData.scope === 'com.atproto.signupQueued' 22 + ) 23 + } catch { 24 + return false 25 + } 22 26 } 23 27 return false 24 28 }