Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

fix: account switchers & text edits

- use the right account switcher on mobile
- don't use white backgrounds for accounts in Settings account switcher
- removed extra language from dialogs
- prepare for account switchers for every interaction mode

+230 -100
+42 -30
src/components/AccountList.tsx
··· 24 24 import {useHiddenAccountsElsewhere} from '#/storage/hooks/hidden-accounts-elsewhere' 25 25 26 26 export function AccountList({ 27 + accounts: accountsProp, 27 28 onSelectAccount, 28 29 onSelectOther, 29 30 otherLabel, 30 31 pendingDid, 32 + selectedDid, 33 + showAddAccount = true, 31 34 }: { 35 + accounts?: SessionAccount[] 32 36 onSelectAccount: (account: SessionAccount) => void 33 37 onSelectOther: () => void 34 38 otherLabel?: string 35 39 pendingDid: string | null 40 + selectedDid?: string | null 41 + showAddAccount?: boolean 36 42 }) { 37 - const {currentAccount, accounts} = useSession() 43 + const {currentAccount, accounts: sessionAccounts} = useSession() 38 44 const t = useTheme() 39 45 const {_} = useLingui() 40 46 const enableSquareButtons = useEnableSquareButtons() 47 + const accounts = accountsProp ?? sessionAccounts 41 48 const [, , hiddenDidsSet] = useHiddenAccountsElsewhere() 42 49 const {data: profiles} = useProfilesQuery({ 43 50 handles: accounts.map(acc => acc.did), ··· 65 72 profile={profiles?.profiles.find(p => p.did === account.did)} 66 73 account={account} 67 74 onSelect={onSelectAccount} 68 - isCurrentAccount={account.did === currentAccount?.did} 75 + isCurrentAccount={ 76 + account.did === (selectedDid ?? currentAccount?.did) 77 + } 69 78 isPendingAccount={account.did === pendingDid} 70 79 /> 71 80 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 72 81 </Fragment> 73 82 ))} 74 - <Button 75 - testID="chooseAddAccountBtn" 76 - style={[a.flex_1]} 77 - onPress={pendingDid ? undefined : onPressAddAccount} 78 - label={_(msg`Sign in to account that is not listed`)}> 79 - {({hovered, pressed}) => ( 80 - <View 81 - style={[ 82 - a.flex_1, 83 - a.flex_row, 84 - a.align_center, 85 - a.p_lg, 86 - a.gap_sm, 87 - (hovered || pressed) && t.atoms.bg_contrast_25, 88 - ]}> 83 + {showAddAccount ? ( 84 + <Button 85 + testID="chooseAddAccountBtn" 86 + style={[a.flex_1]} 87 + onPress={pendingDid ? undefined : onPressAddAccount} 88 + label={_(msg`Sign in to account that is not listed`)}> 89 + {({hovered, pressed}) => ( 89 90 <View 90 91 style={[ 91 - t.atoms.bg_contrast_25, 92 - enableSquareButtons ? a.rounded_sm : a.rounded_full, 93 - {width: 48, height: 48}, 94 - a.justify_center, 92 + a.flex_1, 93 + a.flex_row, 95 94 a.align_center, 96 - (hovered || pressed) && t.atoms.bg_contrast_50, 95 + a.p_lg, 96 + a.gap_sm, 97 + (hovered || pressed) && t.atoms.bg_contrast_25, 97 98 ]}> 98 - <PlusIcon style={[t.atoms.text_contrast_low]} size="md" /> 99 + <View 100 + style={[ 101 + t.atoms.bg_contrast_25, 102 + enableSquareButtons ? a.rounded_sm : a.rounded_full, 103 + {width: 48, height: 48}, 104 + a.justify_center, 105 + a.align_center, 106 + (hovered || pressed) && t.atoms.bg_contrast_50, 107 + ]}> 108 + <PlusIcon style={[t.atoms.text_contrast_low]} size="md" /> 109 + </View> 110 + <Text 111 + style={[a.flex_1, a.leading_tight, a.text_md, a.font_medium]}> 112 + {otherLabel ?? <Trans>Other account</Trans>} 113 + </Text> 114 + <ChevronIcon size="md" style={[t.atoms.text_contrast_low]} /> 99 115 </View> 100 - <Text style={[a.flex_1, a.leading_tight, a.text_md, a.font_medium]}> 101 - {otherLabel ?? <Trans>Other account</Trans>} 102 - </Text> 103 - <ChevronIcon size="md" style={[t.atoms.text_contrast_low]} /> 104 - </View> 105 - )} 106 - </Button> 116 + )} 117 + </Button> 118 + ) : null} 107 119 </View> 108 120 ) 109 121 }
+104
src/components/EphemeralAccountSwitcher.tsx
··· 1 + import {useMemo} from 'react' 2 + import {type AppBskyActorDefs} from '@atproto/api' 3 + import {useLingui} from '@lingui/react/macro' 4 + 5 + import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 6 + import {type SessionAccount, useSession} from '#/state/session' 7 + import {SwitchMenuItems} from '#/view/shell/desktop/LeftNav' 8 + import {useDialogControl} from '#/components/Dialog' 9 + import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 10 + import * as Menu from '#/components/Menu' 11 + import {type TriggerChildProps} from '#/components/Menu/types' 12 + import * as Prompt from '#/components/Prompt' 13 + import {IS_WEB_TOUCH_DEVICE} from '#/env' 14 + 15 + type AccountListItem = { 16 + account: SessionAccount 17 + profile?: AppBskyActorDefs.ProfileViewDetailed 18 + } 19 + 20 + export function EphemeralAccountSwitcher({ 21 + selectedDid, 22 + title, 23 + onSelectAccount, 24 + renderTrigger, 25 + }: { 26 + selectedDid: string 27 + title: string 28 + onSelectAccount: (account: SessionAccount) => void 29 + renderTrigger: (args: { 30 + currentProfile?: AppBskyActorDefs.ProfileViewDetailed 31 + triggerProps: TriggerChildProps['props'] 32 + }) => React.ReactNode 33 + }) { 34 + const {t: l} = useLingui() 35 + const {accounts} = useSession() 36 + const {data: currentProfile} = useProfileQuery({did: selectedDid}) 37 + const {data} = useProfilesQuery({ 38 + handles: accounts.map(acc => acc.did), 39 + }) 40 + const control = useDialogControl() 41 + const signOutPromptControl = Prompt.usePromptControl() 42 + const profiles = data?.profiles 43 + 44 + const switcherAccounts = useMemo<AccountListItem[]>( 45 + () => 46 + accounts 47 + .filter(account => account.did !== selectedDid) 48 + .map(account => ({ 49 + account, 50 + profile: profiles?.find(p => p.did === account.did), 51 + })), 52 + [accounts, profiles, selectedDid], 53 + ) 54 + 55 + if (IS_WEB_TOUCH_DEVICE) { 56 + return ( 57 + <> 58 + {renderTrigger({ 59 + currentProfile, 60 + triggerProps: { 61 + ref: null, 62 + onPress: control.open, 63 + onFocus: () => {}, 64 + onBlur: () => {}, 65 + onPressIn: () => {}, 66 + onPressOut: () => {}, 67 + accessibilityLabel: l`Switch accounts`, 68 + accessibilityRole: 'button', 69 + }, 70 + })} 71 + <SwitchAccountDialog 72 + control={control} 73 + accounts={switcherAccounts.map(item => item.account)} 74 + pendingDid={null} 75 + selectedDid={selectedDid} 76 + title={title} 77 + showAddAccount={false} 78 + onSelectAccount={onSelectAccount} 79 + /> 80 + </> 81 + ) 82 + } 83 + 84 + return ( 85 + <Menu.Root> 86 + <Menu.Trigger label={l`Switch accounts`}> 87 + {({props}) => 88 + renderTrigger({ 89 + currentProfile, 90 + triggerProps: props, 91 + }) 92 + } 93 + </Menu.Trigger> 94 + <SwitchMenuItems 95 + accounts={switcherAccounts} 96 + signOutPromptControl={signOutPromptControl} 97 + showExtraButtons={false} 98 + showAddAccount={false} 99 + title={title} 100 + onSelectAccount={onSelectAccount} 101 + /> 102 + </Menu.Root> 103 + ) 104 + }
+2 -2
src/components/PetAccountAlert.tsx
··· 23 23 24 24 const isSelf = profile.did === currentAccount?.did 25 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.` 26 + ? l`You have marked this account as a pet. You can remove this label at any time from your account settings.` 27 + : l`This account has been marked as a pet by its owner.` 28 28 29 29 return ( 30 30 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
+36 -5
src/components/dialogs/SwitchAccount.tsx
··· 14 14 15 15 export function SwitchAccountDialog({ 16 16 control, 17 + accounts, 18 + title, 19 + pendingDid: pendingDidProp, 20 + selectedDid, 21 + otherLabel, 22 + showAddAccount = true, 23 + onSelectAccount: onSelectAccountProp, 24 + onSelectOther: onSelectOtherProp, 17 25 }: { 18 26 control: Dialog.DialogControlProps 27 + accounts?: SessionAccount[] 28 + title?: string 29 + pendingDid?: string | null 30 + selectedDid?: string | null 31 + otherLabel?: string 32 + showAddAccount?: boolean 33 + onSelectAccount?: (account: SessionAccount) => void 34 + onSelectOther?: () => void 19 35 }) { 20 36 const {_} = useLingui() 21 37 const {currentAccount} = useSession() ··· 24 40 25 41 const onSelectAccount = useCallback( 26 42 (account: SessionAccount) => { 43 + if (onSelectAccountProp) { 44 + control.close(() => { 45 + onSelectAccountProp(account) 46 + }) 47 + return 48 + } 27 49 if (account.did !== currentAccount?.did) { 28 50 control.close(() => { 29 51 onPressSwitchAccount(account, 'SwitchAccount') ··· 32 54 control.close() 33 55 } 34 56 }, 35 - [currentAccount, control, onPressSwitchAccount], 57 + [control, currentAccount, onPressSwitchAccount, onSelectAccountProp], 36 58 ) 37 59 38 60 const onPressAddAccount = useCallback(() => { 61 + if (onSelectOtherProp) { 62 + control.close(() => { 63 + onSelectOtherProp() 64 + }) 65 + return 66 + } 39 67 control.close(() => { 40 68 setShowLoggedOut(true) 41 69 }) 42 - }, [setShowLoggedOut, control]) 70 + }, [control, onSelectOtherProp, setShowLoggedOut]) 43 71 44 72 return ( 45 73 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> ··· 47 75 <Dialog.ScrollableInner label={_(msg`Switch account`)}> 48 76 <View style={[a.gap_lg]}> 49 77 <Text style={[a.text_2xl, a.font_semi_bold]}> 50 - <Trans>Switch account</Trans> 78 + {title ?? <Trans>Switch account</Trans>} 51 79 </Text> 52 80 53 81 <AccountList 82 + accounts={accounts} 54 83 onSelectAccount={onSelectAccount} 55 84 onSelectOther={onPressAddAccount} 56 - otherLabel={_(msg`Add account`)} 57 - pendingDid={pendingDid} 85 + otherLabel={otherLabel ?? _(msg`Add account`)} 86 + pendingDid={pendingDidProp ?? pendingDid} 87 + selectedDid={selectedDid} 88 + showAddAccount={showAddAccount} 58 89 /> 59 90 </View> 60 91
+2 -2
src/screens/Settings/RunesSettings/BadgesSettings.tsx
··· 325 325 onPress={onToggleExpanded}> 326 326 <SettingsList.ItemIcon icon={VerifiedIcon} /> 327 327 <SettingsList.ItemText> 328 - <Trans>{`Trusted Verifiers`}</Trans> 328 + <Trans>{`Trusted verifiers`}</Trans> 329 329 </SettingsList.ItemText> 330 330 {!isExpanded && ( 331 331 <SettingsList.BadgeText> ··· 387 387 <Prompt.Basic 388 388 control={resetTrustedVerifiersPromptControl} 389 389 title={l`Are you sure?`} 390 - description={l`This will clear your selected trusted verifiers and trust your current account again.`} 390 + description={l`This will clear your selected trusted verifiers, resulting in only your account being active.`} 391 391 confirmButtonCta={l`Reset all`} 392 392 confirmButtonColor="negative" 393 393 onConfirm={onResetAll}
+3 -2
src/screens/Settings/Settings.tsx
··· 1 1 import {useState} from 'react' 2 2 import {Alert, LayoutAnimation, Pressable, View} from 'react-native' 3 - import Animated, { 3 + import type Animated from 'react-native-reanimated' 4 + import { 4 5 useAnimatedRef, 5 6 useReducedMotion, 6 7 useScrollViewOffset, ··· 856 857 } 857 858 858 859 return ( 859 - <View style={[a.relative, {backgroundColor: t.palette.white}]}> 860 + <View style={[a.relative, t.atoms.bg]}> 860 861 <SettingsList.PressableItem 861 862 onPress={onSwitchAccount} 862 863 label={l`Switch account`}
+23 -49
src/view/com/composer/Composer.tsx
··· 108 108 useOpenRouterModel, 109 109 } from '#/state/preferences/openrouter' 110 110 import {usePreferencesQuery} from '#/state/queries/preferences' 111 - import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 112 111 import {type Gif} from '#/state/queries/tenor' 113 112 import {useAgent, useSession, useSessionApi} from '#/state/session' 114 113 import {useComposerControls} from '#/state/shell/composer' ··· 137 136 import {VideoPreview} from '#/view/com/composer/videos/VideoPreview' 138 137 import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress' 139 138 import {UserAvatar} from '#/view/com/util/UserAvatar' 140 - import {SwitchMenuItems} from '#/view/shell/desktop/LeftNav' 141 139 import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' 142 140 import {Admonition} from '#/components/Admonition' 143 141 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 144 142 import * as EmojiPicker from '#/components/EmojiPicker' 143 + import {EphemeralAccountSwitcher} from '#/components/EphemeralAccountSwitcher' 145 144 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo' 146 145 import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji' 147 146 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 148 147 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 149 - import * as Menu from '#/components/Menu' 150 148 import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed' 151 149 import * as Prompt from '#/components/Prompt' 152 150 import * as Toast from '#/components/Toast' ··· 1476 1474 activeAccountDid: string 1477 1475 setActiveAccountDid: (did: string) => void 1478 1476 }) { 1479 - const {accounts} = useSession() 1480 1477 const {t: l} = useLingui() 1481 - const {data: currentProfile} = useProfileQuery({did: activeAccountDid}) 1482 1478 const richtext = post.richtext 1483 1479 const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media 1484 1480 const forceMinHeight = IS_WEB && isTextOnly && isActive ··· 1488 1484 : l`Add another post` 1489 1485 : l`Anything but skeet` 1490 1486 const discardPromptControl = Prompt.usePromptControl() 1491 - const signOutPromptControl = Prompt.usePromptControl() 1492 1487 1493 1488 const enableSquareButtons = useEnableSquareButtons() 1494 1489 ··· 1547 1542 [post.id, onSelectVideo, onImageAdd, l], 1548 1543 ) 1549 1544 1550 - const {data} = useProfilesQuery({ 1551 - handles: accounts.map(acc => acc.did), 1552 - }) 1553 - const profiles = data?.profiles 1554 - 1555 - const allAccounts = accounts 1556 - .filter(account => account.did !== activeAccountDid) 1557 - .map(account => ({ 1558 - account, 1559 - profile: profiles?.find(p => p.did === account.did), 1560 - })) 1561 - 1562 1545 useHideKeyboardOnBackground() 1563 1546 1564 1547 return ( ··· 1571 1554 isTextOnly && isLastPost && IS_NATIVE && a.flex_grow, 1572 1555 ]}> 1573 1556 <View style={[a.flex_row, IS_NATIVE && a.flex_1]}> 1574 - <Menu.Root> 1575 - <Menu.Trigger label={l`Switch accounts`}> 1576 - {({props}) => ( 1577 - <Button 1578 - label={props.accessibilityLabel} 1579 - {...props} 1580 - style={[ 1581 - a.transition_color, 1582 - enableSquareButtons ? a.rounded_sm : a.rounded_full, 1583 - a.self_start, 1584 - ]}> 1585 - <UserAvatar 1586 - avatar={currentProfile?.avatar} 1587 - size={42} 1588 - type={ 1589 - currentProfile?.associated?.labeler ? 'labeler' : 'user' 1590 - } 1591 - style={[a.mt_xs]} 1592 - /> 1593 - </Button> 1594 - )} 1595 - </Menu.Trigger> 1596 - { 1597 - <SwitchMenuItems 1598 - accounts={allAccounts} 1599 - signOutPromptControl={signOutPromptControl} 1600 - showExtraButtons={false} 1601 - onSelectAccount={account => setActiveAccountDid(account.did)} 1602 - /> 1603 - } 1604 - </Menu.Root> 1557 + <EphemeralAccountSwitcher 1558 + selectedDid={activeAccountDid} 1559 + title={l`Post from account`} 1560 + onSelectAccount={account => setActiveAccountDid(account.did)} 1561 + renderTrigger={({currentProfile, triggerProps}) => ( 1562 + <Button 1563 + label={triggerProps.accessibilityLabel} 1564 + {...triggerProps} 1565 + style={[ 1566 + a.transition_color, 1567 + enableSquareButtons ? a.rounded_sm : a.rounded_full, 1568 + a.self_start, 1569 + ]}> 1570 + <UserAvatar 1571 + avatar={currentProfile?.avatar} 1572 + size={42} 1573 + type={currentProfile?.associated?.labeler ? 'labeler' : 'user'} 1574 + style={[a.mt_xs]} 1575 + /> 1576 + </Button> 1577 + )} 1578 + /> 1605 1579 <TextInput 1606 1580 ref={textInputRef} 1607 1581 style={[a.pt_xs]}
+18 -10
src/view/shell/desktop/LeftNav.tsx
··· 233 233 accounts, 234 234 signOutPromptControl, 235 235 showExtraButtons, 236 + showAddAccount, 237 + title, 236 238 onSelectAccount, 237 239 }: { 238 240 accounts: ··· 243 245 | undefined 244 246 signOutPromptControl: DialogControlProps 245 247 showExtraButtons?: boolean 248 + showAddAccount?: boolean 249 + title?: string 246 250 onSelectAccount?: (account: SessionAccount) => void 247 251 }) { 248 252 const {_} = useLingui() ··· 254 258 ) 255 259 256 260 showExtraButtons = showExtraButtons ?? true 261 + showAddAccount = showAddAccount ?? true 262 + const hasFooterItems = showExtraButtons || showAddAccount 257 263 258 264 const onAddAnotherAccount = () => { 259 265 setShowLoggedOut(true) ··· 266 272 <> 267 273 <Menu.Group> 268 274 <Menu.LabelText> 269 - <Trans>Switch account</Trans> 275 + {title ?? <Trans>Switch account</Trans>} 270 276 </Menu.LabelText> 271 277 {sortedAccounts.map(other => ( 272 278 <SwitchMenuItem ··· 277 283 /> 278 284 ))} 279 285 </Menu.Group> 280 - <Menu.Divider /> 286 + {hasFooterItems ? <Menu.Divider /> : null} 281 287 </> 282 288 )} 283 289 {showExtraButtons ? <SwitcherMenuProfileLink /> : undefined} 284 - <Menu.Item 285 - label={_(msg`Add another account`)} 286 - onPress={onAddAnotherAccount}> 287 - <Menu.ItemIcon icon={PlusIcon} /> 288 - <Menu.ItemText> 289 - <Trans>Add another account</Trans> 290 - </Menu.ItemText> 291 - </Menu.Item> 290 + {showAddAccount ? ( 291 + <Menu.Item 292 + label={_(msg`Add another account`)} 293 + onPress={onAddAnotherAccount}> 294 + <Menu.ItemIcon icon={PlusIcon} /> 295 + <Menu.ItemText> 296 + <Trans>Add another account</Trans> 297 + </Menu.ItemText> 298 + </Menu.Item> 299 + ) : null} 292 300 {showExtraButtons ? ( 293 301 <Menu.Item label={_(msg`Sign out`)} onPress={signOutPromptControl.open}> 294 302 <Menu.ItemIcon icon={LeaveIcon} />