diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 9cb1dadb5..0a58ac031 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -131,13 +131,14 @@ import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSec import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings' import {RunesDisplayAlsoLikedSettingsScreen} from '#/screens/Settings/RunesSettings/AlsoLikedSettings' import {RunesBadgesSettingsScreen} from '#/screens/Settings/RunesSettings/BadgesSettings' +import {RunesDisplayDensitySettingsScreen} from '#/screens/Settings/RunesSettings/DensitySettings' import {RunesDisplaySettingsScreen} from '#/screens/Settings/RunesSettings/DisplaySettings' -import {RunesSettingsSyncSettingsScreen} from '#/screens/Settings/RunesSettings/SettingsSyncSettings' import {RunesExtraSettingsScreen} from '#/screens/Settings/RunesSettings/ExtraSettings' import {RunesUsabilityFeedSettingsScreen} from '#/screens/Settings/RunesSettings/FeedSettings' import {RunesImpressionsSettingsScreen} from '#/screens/Settings/RunesSettings/ImpressionsSettings' import {RunesInfrastructureSettingsScreen} from '#/screens/Settings/RunesSettings/InfrastructureSettings' import {RunesMenusSettingsScreen} from '#/screens/Settings/RunesSettings/MenusSettings' +import {RunesSettingsSyncSettingsScreen} from '#/screens/Settings/RunesSettings/SettingsSyncSettings' import {RunesUsabilitySettingsScreen} from '#/screens/Settings/RunesSettings/UsabilitySettings' import {SettingsScreen} from '#/screens/Settings/Settings' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' @@ -470,6 +471,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => RunesDisplayAlsoLikedSettingsScreen} options={{title: title(msg`Also liked`), requireAuth: true}} /> + RunesDisplayDensitySettingsScreen} + options={{title: title(msg`Density`), requireAuth: true}} + /> RunesInfrastructureSettingsScreen} diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index 27028c056..766530a8c 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -8,6 +8,8 @@ import {Trans} from '@lingui/react/macro' import {isJwtExpired} from '#/lib/jwt' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {useAutoCompactAccountSwitcher} from '#/state/preferences/auto-compact-account-switcher' +import {useCompactAccountSwitcher} from '#/state/preferences/compact-account-switcher' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {useProfilesQuery} from '#/state/queries/profile' import {type SessionAccount, useSession} from '#/state/session' @@ -44,6 +46,8 @@ export function AccountList({ const t = useTheme() const {_} = useLingui() const enableSquareButtons = useEnableSquareButtons() + const useCompactSwitcher = useCompactAccountSwitcher() + const autoCompactSwitcher = useAutoCompactAccountSwitcher() const accounts = accountsProp ?? sessionAccounts const [, , hiddenDidsSet] = useHiddenAccountsElsewhere() const {data: profiles} = useProfilesQuery({ @@ -52,6 +56,9 @@ export function AccountList({ const sortedAccounts = useSortedAccountItems(accounts).filter( account => !hiddenDidsSet.has(account.did), ) + const shouldUseCompactSwitcher = + Boolean(useCompactSwitcher) || + (Boolean(autoCompactSwitcher) && sortedAccounts.length > 6) const onPressAddAccount = useCallback(() => { onSelectOther() @@ -61,7 +68,7 @@ export function AccountList({ p.did === account.did)} account={account} onSelect={onSelectAccount} + useCompactSwitcher={shouldUseCompactSwitcher} isCurrentAccount={ account.did === (selectedDid ?? currentAccount?.did) } @@ -92,26 +100,51 @@ export function AccountList({ a.flex_1, a.flex_row, a.align_center, - a.p_lg, - a.gap_sm, + shouldUseCompactSwitcher ? {height: 48} : a.p_lg, + shouldUseCompactSwitcher ? null : a.gap_sm, (hovered || pressed) && t.atoms.bg_contrast_25, ]}> - - - - - {otherLabel ?? Other account} - - + {shouldUseCompactSwitcher ? ( + <> + + {otherLabel ?? Other account} + + + + ) : ( + <> + + + + + {otherLabel ?? Other account} + + + + )} )} @@ -124,12 +157,14 @@ function AccountItem({ profile, account, onSelect, + useCompactSwitcher, isCurrentAccount, isPendingAccount, }: { profile?: AppBskyActorDefs.ProfileViewDetailed account: SessionAccount onSelect: (account: SessionAccount) => void + useCompactSwitcher: boolean isCurrentAccount: boolean isPendingAccount: boolean }) { @@ -163,13 +198,14 @@ function AccountItem({ a.flex_1, a.flex_row, a.align_center, - a.p_lg, + useCompactSwitcher ? a.px_md : a.p_lg, a.gap_sm, + useCompactSwitcher ? {height: 56} : null, (hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25, ]}> {sanitizeDisplayName( profile?.displayName || profile?.handle || account.handle, @@ -197,7 +237,7 @@ function AccountItem({ style={[ a.leading_tight, t.atoms.text_contrast_medium, - a.text_sm, + useCompactSwitcher ? null : a.text_sm, ]}> {sanitizeHandle(account.handle, '@')} @@ -215,21 +255,30 @@ function AccountItem({ {isCurrentAccount ? ( - - - + useCompactSwitcher ? ( + + ) : ( + + + + ) ) : ( - + )} )} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 607119020..4c87e529c 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -59,6 +59,7 @@ export type CommonNavigatorParams = { RunesUsabilityFeedSettings: undefined RunesDisplaySettings: undefined RunesDisplayAlsoLikedSettings: undefined + RunesDisplayDensitySettings: undefined RunesInfrastructureSettings: undefined RunesExtraSettings: undefined RunesSettingsSyncSettings: undefined diff --git a/src/routes.ts b/src/routes.ts index 572c2f1c9..070abaadd 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -56,6 +56,7 @@ export const router = new Router({ RunesUsabilityFeedSettings: '/settings/runes/usability/feeds', RunesDisplaySettings: '/settings/runes/display', RunesDisplayAlsoLikedSettings: '/settings/runes/display/also-liked', + RunesDisplayDensitySettings: '/settings/runes/display/density', RunesInfrastructureSettings: '/settings/runes/infrastructure', RunesExtraSettings: '/settings/runes/extra', RunesSettingsSyncSettings: '/settings/runes/settings-sync', diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index 4149f5f5c..ca794b6a5 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -60,7 +60,6 @@ export function AppearanceSettingsScreen({}: Props) { const enableSquareButtons = useEnableSquareButtons() const setEnableSquareButtons = useSetEnableSquareButtons() - const colorSchemes = useColorSchemes() const colorSchemeLabel = getColorSchemeLabel(colorSchemes, colorScheme) @@ -254,7 +253,6 @@ export function AppearanceSettingsScreen({}: Props) { - {IS_NATIVE && IS_INTERNAL && ( <> diff --git a/src/screens/Settings/RunesSettings/DensitySettings.tsx b/src/screens/Settings/RunesSettings/DensitySettings.tsx new file mode 100644 index 000000000..21d011bab --- /dev/null +++ b/src/screens/Settings/RunesSettings/DensitySettings.tsx @@ -0,0 +1,61 @@ +import {Trans, useLingui} from '@lingui/react/macro' + +import { + useAutoCompactAccountSwitcher, + useSetAutoCompactAccountSwitcher, +} from '#/state/preferences/auto-compact-account-switcher' +import { + useCompactAccountSwitcher, + useSetCompactAccountSwitcher, +} from '#/state/preferences/compact-account-switcher' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesDisplayDensitySettingsScreen() { + const {t: l} = useLingui() + + const compactAccountSwitcher = useCompactAccountSwitcher() + const setCompactAccountSwitcher = useSetCompactAccountSwitcher() + + const autoCompactAccountSwitcher = useAutoCompactAccountSwitcher() + const setAutoCompactAccountSwitcher = useSetAutoCompactAccountSwitcher() + + return ( + + + + + Account switcher + + setCompactAccountSwitcher(value)} + style={[a.w_full]}> + + Use compact account switcher + + + + setAutoCompactAccountSwitcher(value)} + style={[a.w_full]}> + + + Automatically use compact account switcher with more than 6 + accounts + + + + + + + ) +} diff --git a/src/screens/Settings/RunesSettings/DisplaySettings.tsx b/src/screens/Settings/RunesSettings/DisplaySettings.tsx index 5fb1f0d2d..a77b88cd2 100644 --- a/src/screens/Settings/RunesSettings/DisplaySettings.tsx +++ b/src/screens/Settings/RunesSettings/DisplaySettings.tsx @@ -10,6 +10,8 @@ import { useAlsoLikedCollapseByDefault, useAlsoLikedFeedEnabled, } from '#/state/preferences' +import {useAutoCompactAccountSwitcher} from '#/state/preferences/auto-compact-account-switcher' +import {useCompactAccountSwitcher} from '#/state/preferences/compact-account-switcher' import { useHighQualityImages, useSetHighQualityImages, @@ -35,6 +37,7 @@ import * as Toggle from '#/components/forms/Toggle' import {Heart2_Stroke2_Corner0_Rounded as HeartIcon} from '#/components/icons/Heart2' import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' +import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/Repost' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import {Text} from '#/components/Typography' @@ -76,6 +79,16 @@ export function RunesDisplaySettingsScreen() { } /> + + + Density} + subtitleText={} + /> + Shown in thread views, expanded by default } +function AccountSwitcherDeclaration() { + const compactAccountSwitcher = useCompactAccountSwitcher() + const autoCompactAccountSwitcher = useAutoCompactAccountSwitcher() + + if (compactAccountSwitcher) { + return Compact always on + } + + if (autoCompactAccountSwitcher) { + return Auto-compact with 7+ accounts + } + + return Default layout only +} + function PostReplacementDialog({ control, }: { diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index a7eb2d37c..dd50fe8cc 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -196,6 +196,8 @@ const schema = z.object({ discoverContextEnabled: z.boolean().optional(), enableSquareAvatars: z.boolean().optional(), enableSquareButtons: z.boolean().optional(), + useCompactAccountSwitcher: z.boolean().optional(), + autoCompactAccountSwitcher: z.boolean().optional(), disableVerifyEmailReminder: z.boolean().optional(), showViaClient: z.boolean().optional(), deerVerification: z @@ -335,6 +337,8 @@ export const defaults: Schema = { discoverContextEnabled: false, enableSquareAvatars: true, enableSquareButtons: true, + useCompactAccountSwitcher: false, + autoCompactAccountSwitcher: true, disableVerifyEmailReminder: false, showViaClient: true, deerVerification: { diff --git a/src/state/preferences/auto-compact-account-switcher.tsx b/src/state/preferences/auto-compact-account-switcher.tsx new file mode 100644 index 000000000..5a786a4a1 --- /dev/null +++ b/src/state/preferences/auto-compact-account-switcher.tsx @@ -0,0 +1,56 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['autoCompactAccountSwitcher'] +type SetContext = (v: persisted.Schema['autoCompactAccountSwitcher']) => void + +const stateContext = createContext( + persisted.defaults.autoCompactAccountSwitcher, +) +const setContext = createContext( + (_: persisted.Schema['autoCompactAccountSwitcher']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState( + persisted.get('autoCompactAccountSwitcher'), + ) + + const setStateWrapped = useCallback( + (value: persisted.Schema['autoCompactAccountSwitcher']) => { + setState(value) + persisted.write('autoCompactAccountSwitcher', value) + }, + [], + ) + + useEffect(() => { + return persisted.onUpdate('autoCompactAccountSwitcher', next => { + setState(next) + }) + }, []) + + return ( + + + {children} + + + ) +} + +export function useAutoCompactAccountSwitcher() { + return useContext(stateContext) +} + +export function useSetAutoCompactAccountSwitcher() { + return useContext(setContext) +} diff --git a/src/state/preferences/compact-account-switcher.tsx b/src/state/preferences/compact-account-switcher.tsx new file mode 100644 index 000000000..ac0d56490 --- /dev/null +++ b/src/state/preferences/compact-account-switcher.tsx @@ -0,0 +1,54 @@ +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = persisted.Schema['useCompactAccountSwitcher'] +type SetContext = (v: persisted.Schema['useCompactAccountSwitcher']) => void + +const stateContext = createContext( + persisted.defaults.useCompactAccountSwitcher, +) +const setContext = createContext( + (_: persisted.Schema['useCompactAccountSwitcher']) => {}, +) + +export function Provider({children}: PropsWithChildren<{}>) { + const [state, setState] = useState(persisted.get('useCompactAccountSwitcher')) + + const setStateWrapped = useCallback( + (value: persisted.Schema['useCompactAccountSwitcher']) => { + setState(value) + persisted.write('useCompactAccountSwitcher', value) + }, + [], + ) + + useEffect(() => { + return persisted.onUpdate('useCompactAccountSwitcher', next => { + setState(next) + }) + }, []) + + return ( + + + {children} + + + ) +} + +export function useCompactAccountSwitcher() { + return useContext(stateContext) +} + +export function useSetCompactAccountSwitcher() { + return useContext(setContext) +} diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 9cfb71f62..6cd6ee78c 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -3,9 +3,10 @@ import {type PropsWithChildren} from 'react' import {Provider as AlsoLikedCollapseByDefaultProvider} from './also-liked-collapse-by-default' import {Provider as AlsoLikedFeedProvider} from './also-liked-feed-enabled' import {Provider as AltTextRequiredProvider} from './alt-text-required' +import {Provider as AutoCompactAccountSwitcherProvider} from './auto-compact-account-switcher' import {Provider as AutoLikeOnRepostProvider} from './auto-like-on-repost' -import {Provider as SettingsSyncProvider} from './settings-sync' import {Provider as AutoplayProvider} from './autoplay' +import {Provider as CompactAccountSwitcherProvider} from './compact-account-switcher' import {Provider as ConstellationProvider} from './constellation-enabled' import {Provider as ConstellationInstanceProvider} from './constellation-instance' import {Provider as DeerVerificationProvider} from './deer-verification' @@ -50,6 +51,7 @@ import {Provider as PdsLabelProvider} from './pds-label' import {Provider as PlcDirectoryProvider} from './plc-directory' import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx' import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' +import {Provider as SettingsSyncProvider} from './settings-sync' import {Provider as ShowFollowsYouBadgeProvider} from './show-follows-you-badge' import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' import {Provider as ShowLinkInHandleOnlyOnWorkingLinksProvider} from './show-link-in-handle-only-on-working-links' @@ -72,7 +74,6 @@ export { useRequireAltTextEnabled, useSetRequireAltTextEnabled, } from './alt-text-required' -export {useSettingsSyncEnabled, useSetSettingsSyncEnabled} from './settings-sync' export {useAutoplayDisabled, useSetAutoplayDisabled} from './autoplay' export { useDisableComposerPrompt, @@ -118,6 +119,10 @@ export { usePlcDirectory, useSetPlcDirectory, } from './plc-directory' +export { + useSetSettingsSyncEnabled, + useSettingsSyncEnabled, +} from './settings-sync' export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles' export { useSetTranslationServicePreference, @@ -127,125 +132,129 @@ export { export function Provider({children}: PropsWithChildren<{}>) { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - children - } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + children + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/src/state/preferences/settings-sync.tsx b/src/state/preferences/settings-sync.tsx index 420c2f426..f31e5a762 100644 --- a/src/state/preferences/settings-sync.tsx +++ b/src/state/preferences/settings-sync.tsx @@ -5,10 +5,10 @@ import { useEffect, useState, } from 'react' -import type {PropsWithChildren} from 'react' +import {type PropsWithChildren} from 'react' import * as persisted from '#/state/persisted' -import type {Schema} from '#/state/persisted/schema' +import {type Schema} from '#/state/persisted/schema' // --------------------------------------------------------------------------- // Synced keys allowlist @@ -65,6 +65,8 @@ export const SYNCED_PREFS_KEYS = [ 'discoverContextEnabled', 'enableSquareAvatars', 'enableSquareButtons', + 'useCompactAccountSwitcher', + 'autoCompactAccountSwitcher', 'disableVerifyEmailReminder', 'showViaClient', 'deerVerification', @@ -115,13 +117,10 @@ export function Provider({children}: PropsWithChildren<{}>) { Boolean(persisted.get('settingsSyncEnabled')), ) - const setStateWrapped = useCallback( - (value: boolean) => { - setState(value) - persisted.write('settingsSyncEnabled', value) - }, - [], - ) + const setStateWrapped = useCallback((value: boolean) => { + setState(value) + persisted.write('settingsSyncEnabled', value) + }, []) useEffect(() => { return persisted.onUpdate('settingsSyncEnabled', next => {