diff options
Diffstat (limited to 'src/view/screens')
33 files changed, 1872 insertions, 214 deletions
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index 154035f22..dc439c367 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -33,6 +33,7 @@ import {cleanError} from '#/lib/strings/errors' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> export function AppPasswords({}: Props) { const pal = usePalette('default') + const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() const {isTabletOrDesktop} = useWebMediaQueries() @@ -61,8 +62,8 @@ export function AppPasswords({}: Props) { ]} testID="appPasswordsScreen"> <ErrorScreen - title="Oops!" - message="There was an issue with fetching your app passwords" + title={_(msg`Oops!`)} + message={_(msg`There was an issue with fetching your app passwords`)} details={cleanError(error)} /> </CenteredView> @@ -98,7 +99,7 @@ export function AppPasswords({}: Props) { <Button testID="appPasswordBtn" type="primary" - label="Add App Password" + label={_(msg`Add App Password`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={onAdd} @@ -139,7 +140,7 @@ export function AppPasswords({}: Props) { <Button testID="appPasswordBtn" type="primary" - label="Add App Password" + label={_(msg`Add App Password`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={onAdd} @@ -152,7 +153,7 @@ export function AppPasswords({}: Props) { <Button testID="appPasswordBtn" type="primary" - label="Add App Password" + label={_(msg`Add App Password`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={onAdd} @@ -224,7 +225,7 @@ function AppPassword({ ), async onPressConfirm() { await deleteMutation.mutateAsync({name}) - Toast.show('App password deleted') + Toast.show(_(msg`App password deleted`)) }, }) }, [deleteMutation, openModal, name, _]) diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 0e0464200..f26b1505a 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -16,6 +16,8 @@ import {ToggleButton} from '../com/util/forms/ToggleButton' import {RadioGroup} from '../com/util/forms/RadioGroup' import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorMessage} from '../com/util/error/ErrorMessage' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs'] @@ -48,6 +50,7 @@ function DebugInner({ }) { const [currentView, setCurrentView] = React.useState<number>(0) const pal = usePalette('default') + const {_} = useLingui() const renderItem = (item: any) => { return ( @@ -57,7 +60,7 @@ function DebugInner({ type="default-light" onPress={onToggleColorScheme} isSelected={colorScheme === 'dark'} - label="Dark mode" + label={_(msg`Dark mode`)} /> </View> {item.currentView === 3 ? ( @@ -77,7 +80,7 @@ function DebugInner({ return ( <View style={[s.hContentRegion, pal.view]}> - <ViewHeader title="Debug panel" /> + <ViewHeader title={_(msg`Debug panel`)} /> <ViewSelector swipeEnabled sections={MAIN_VIEWS} diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 20cdf815a..a913364d4 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -97,6 +97,7 @@ export function FeedsScreen(_props: Props) { data: preferences, isLoading: isPreferencesLoading, error: preferencesError, + refetch: refetchPreferences, } = usePreferencesQuery() const { data: popularFeeds, @@ -151,9 +152,12 @@ export function FeedsScreen(_props: Props) { }, [query, debouncedSearch]) const onPullToRefresh = React.useCallback(async () => { setIsPTR(true) - await refetchPopularFeeds() + await Promise.all([ + refetchPreferences().catch(_e => undefined), + refetchPopularFeeds().catch(_e => undefined), + ]) setIsPTR(false) - }, [setIsPTR, refetchPopularFeeds]) + }, [setIsPTR, refetchPreferences, refetchPopularFeeds]) const onEndReached = React.useCallback(() => { if ( isPopularFeedsFetching || @@ -328,7 +332,7 @@ export function FeedsScreen(_props: Props) { hitSlop={10} accessibilityRole="button" accessibilityLabel={_(msg`Edit Saved Feeds`)} - accessibilityHint="Opens screen to edit Saved Feeds"> + accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}> <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> </Link> ) @@ -494,6 +498,8 @@ export function FeedsScreen(_props: Props) { // @ts-ignore our .web version only -prf desktopFixedHeight scrollIndicatorInsets={{right: 1}} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" /> {hasSession && ( diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index b8033f0b4..7d6a40f02 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -109,7 +109,9 @@ function HomeScreenReady({ const homeFeedParams = React.useMemo<FeedParams>(() => { return { mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled), - mergeFeedSources: preferences.feeds.saved, + mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled + ? preferences.feeds.saved + : [], } }, [preferences]) diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index d28db7c6c..bdd5dd9b7 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -61,7 +61,7 @@ export function ListsScreen({}: Props) { <Trans>Public, shareable lists which can drive feeds.</Trans> </Text> </View> - <View> + <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> <Button testID="newUserListBtn" type="default" @@ -73,7 +73,7 @@ export function ListsScreen({}: Props) { }}> <FontAwesomeIcon icon="plus" color={pal.colors.text} /> <Text type="button" style={pal.text}> - <Trans>New</Trans> + <Trans context="action">New</Trans> </Text> </Button> </View> diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index 8680b851b..e727a1fb8 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -50,7 +50,9 @@ export function LogScreen({}: NativeStackScreenProps< style={[styles.entry, pal.border, pal.view]} onPress={toggler(entry.id)} accessibilityLabel={_(msg`View debug entry`)} - accessibilityHint="Opens additional details for a debug entry"> + accessibilityHint={_( + msg`Opens additional details for a debug entry`, + )}> {entry.level === 'debug' ? ( <FontAwesomeIcon icon="info" /> ) : ( diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx index 1bf8db2e0..96bb46cef 100644 --- a/src/view/screens/Moderation.tsx +++ b/src/view/screens/Moderation.tsx @@ -62,7 +62,7 @@ export function ModerationScreen({}: Props) { ]} testID="moderationScreen"> <ViewHeader title={_(msg`Moderation`)} showOnDesktop /> - <ScrollView> + <ScrollView contentContainerStyle={[styles.noBorder]}> <View style={styles.spacer} /> <TouchableOpacity testID="contentFilteringBtn" @@ -275,4 +275,10 @@ const styles = StyleSheet.create({ borderRadius: 30, marginRight: 12, }, + noBorder: { + borderBottomWidth: 0, + borderRightWidth: 0, + borderLeftWidth: 0, + borderTopWidth: 0, + }, }) diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index d6a3b5f6f..b7d993acc 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -63,7 +63,7 @@ export function ModerationModlistsScreen({}: Props) { </Trans> </Text> </View> - <View> + <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}> <Button testID="newModListBtn" type="default" diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 9f50c8b73..276dc842c 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -25,6 +25,7 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage' import {CenteredView} from '../com/util/Views' import {useComposerControls} from '#/state/shell/composer' import {useSession} from '#/state/session' +import {isWeb} from '#/platform/detection' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> export function PostThreadScreen({route}: Props) { @@ -67,6 +68,7 @@ export function PostThreadScreen({route}: Props) { displayName: thread.post.author.displayName, avatar: thread.post.author.avatar, }, + embed: thread.post.embed, }, onPost: () => queryClient.invalidateQueries({ @@ -77,7 +79,9 @@ export function PostThreadScreen({route}: Props) { return ( <View style={s.hContentRegion}> - {isMobile && <ViewHeader title={_(msg`Post`)} />} + {isMobile && ( + <ViewHeader title={_(msg({message: 'Post', context: 'description'}))} /> + )} <View style={s.flex1}> {uriError ? ( <CenteredView> @@ -109,7 +113,8 @@ export function PostThreadScreen({route}: Props) { const styles = StyleSheet.create({ prompt: { - position: 'absolute', + // @ts-ignore web-only + position: isWeb ? 'fixed' : 'absolute', left: 0, right: 0, }, diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx new file mode 100644 index 000000000..1e8cedf7e --- /dev/null +++ b/src/view/screens/PreferencesExternalEmbeds.tsx @@ -0,0 +1,138 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {s} from 'lib/styles' +import {Text} from '../com/util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics/analytics' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import { + EmbedPlayerSource, + externalEmbedLabels, +} from '#/lib/strings/embed-player' +import {useSetMinimalShellMode} from '#/state/shell' +import {Trans} from '@lingui/macro' +import {ScrollView} from '../com/util/Views' +import { + useExternalEmbedsPrefs, + useSetExternalEmbedPref, +} from 'state/preferences' +import {ToggleButton} from 'view/com/util/forms/ToggleButton' +import {SimpleViewHeader} from '../com/util/SimpleViewHeader' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'PreferencesExternalEmbeds' +> +export function PreferencesExternalEmbeds({}: Props) { + const pal = usePalette('default') + const setMinimalShellMode = useSetMinimalShellMode() + const {screen} = useAnalytics() + const {isMobile} = useWebMediaQueries() + + useFocusEffect( + React.useCallback(() => { + screen('PreferencesExternalEmbeds') + setMinimalShellMode(false) + }, [screen, setMinimalShellMode]), + ) + + return ( + <View style={s.hContentRegion} testID="preferencesExternalEmbedsScreen"> + <SimpleViewHeader + showBackButton={isMobile} + style={[ + pal.border, + {borderBottomWidth: 1}, + !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, + ]}> + <View style={{flex: 1}}> + <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> + <Trans>External Media Preferences</Trans> + </Text> + <Text style={pal.textLight}> + <Trans>Customize media from external sites.</Trans> + </Text> + </View> + </SimpleViewHeader> + <ScrollView + // @ts-ignore web only -prf + dataSet={{'stable-gutters': 1}} + contentContainerStyle={[pal.viewLight, {paddingBottom: 200}]}> + <View style={[pal.view]}> + <View style={styles.infoCard}> + <Text style={pal.text}> + <Trans> + External media may allow websites to collect information about + you and your device. No information is sent or requested until + you press the "play" button. + </Trans> + </Text> + </View> + </View> + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Enable media players for</Trans> + </Text> + {Object.entries(externalEmbedLabels).map(([key, label]) => ( + <PrefSelector + source={key as EmbedPlayerSource} + label={label} + key={key} + /> + ))} + </ScrollView> + </View> + ) +} + +function PrefSelector({ + source, + label, +}: { + source: EmbedPlayerSource + label: string +}) { + const pal = usePalette('default') + const setExternalEmbedPref = useSetExternalEmbedPref() + const sources = useExternalEmbedsPrefs() + + return ( + <View> + <View style={[pal.view, styles.toggleCard]}> + <ToggleButton + type="default-light" + label={label} + labelType="lg" + isSelected={sources?.[source] === 'show'} + onPress={() => + setExternalEmbedPref( + source, + sources?.[source] === 'show' ? 'hide' : 'show', + ) + } + /> + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + heading: { + paddingHorizontal: 18, + paddingTop: 14, + paddingBottom: 14, + }, + spacer: { + height: 8, + }, + infoCard: { + paddingHorizontal: 20, + paddingVertical: 14, + }, + toggleCard: { + paddingVertical: 8, + paddingHorizontal: 6, + marginBottom: 1, + }, +}) diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx index 20ef72923..7ad870937 100644 --- a/src/view/screens/PreferencesHomeFeed.tsx +++ b/src/view/screens/PreferencesHomeFeed.tsx @@ -27,8 +27,10 @@ function RepliesThresholdInput({ initialValue: number }) { const pal = usePalette('default') + const {_} = useLingui() const [value, setValue] = useState(initialValue) const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation() + const preValue = React.useRef(initialValue) const save = React.useMemo( () => debounce( @@ -46,7 +48,12 @@ function RepliesThresholdInput({ <Slider value={value} onValueChange={(v: number | number[]) => { - const threshold = Math.floor(Array.isArray(v) ? v[0] : v) + let threshold = Array.isArray(v) ? v[0] : v + if (threshold > preValue.current) threshold = Math.floor(threshold) + else threshold = Math.ceil(threshold) + + preValue.current = threshold + setValue(threshold) save(threshold) }} @@ -58,10 +65,12 @@ function RepliesThresholdInput({ /> <Text type="xs" style={pal.text}> {value === 0 - ? `Show all replies` - : `Show replies with at least ${value} ${ - value > 1 ? `likes` : `like` - }`} + ? _(msg`Show all replies`) + : _( + msg`Show replies with at least ${value} ${ + value > 1 ? `likes` : `like` + }`, + )} </Text> </View> ) diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx index 73d941932..321c67293 100644 --- a/src/view/screens/PreferencesThreads.tsx +++ b/src/view/screens/PreferencesThreads.tsx @@ -75,10 +75,16 @@ export function PreferencesThreads({navigation}: Props) { <RadioGroup type="default-light" items={[ - {key: 'oldest', label: 'Oldest replies first'}, - {key: 'newest', label: 'Newest replies first'}, - {key: 'most-likes', label: 'Most-liked replies first'}, - {key: 'random', label: 'Random (aka "Poster\'s Roulette")'}, + {key: 'oldest', label: _(msg`Oldest replies first`)}, + {key: 'newest', label: _(msg`Newest replies first`)}, + { + key: 'most-likes', + label: _(msg`Most-liked replies first`), + }, + { + key: 'random', + label: _(msg`Random (aka "Poster's Roulette")`), + }, ]} onSelect={key => setThreadViewPrefs({sort: key})} initialSelection={preferences?.threadViewPrefs?.sort} @@ -97,7 +103,7 @@ export function PreferencesThreads({navigation}: Props) { </Text> <ToggleButton type="default-light" - label={prioritizeFollowedUsers ? 'Yes' : 'No'} + label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)} isSelected={prioritizeFollowedUsers} onPress={() => setThreadViewPrefs({ @@ -120,7 +126,7 @@ export function PreferencesThreads({navigation}: Props) { </Text> <ToggleButton type="default-light" - label={treeViewEnabled ? 'Yes' : 'No'} + label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)} isSelected={treeViewEnabled} onPress={() => setThreadViewPrefs({ @@ -153,7 +159,7 @@ export function PreferencesThreads({navigation}: Props) { accessibilityLabel={_(msg`Confirm`)} accessibilityHint=""> <Text style={[s.white, s.bold, s.f18]}> - <Trans>Done</Trans> + <Trans context="action">Done</Trans> </Text> </TouchableOpacity> </View> diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 4558ae33d..7fc4d7a20 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -371,6 +371,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, ref, ) { + const {_} = useLingui() const queryClient = useQueryClient() const [hasNew, setHasNew] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false) @@ -388,8 +389,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( })) const renderPostsEmpty = React.useCallback(() => { - return <EmptyState icon="feed" message="This feed is empty!" /> - }, []) + return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> + }, [_]) return ( <View> @@ -408,7 +409,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( {(isScrolledDown || hasNew) && ( <LoadLatestBtn onPress={onScrollToTop} - label="Load new posts" + label={_(msg`Load new posts`)} showIndicator={hasNew} /> )} diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 211306c0d..61282497c 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -214,11 +214,21 @@ export function ProfileFeedScreenInner({ } } catch (err) { Toast.show( - 'There was an an issue updating your feeds, please check your internet connection and try again.', + _( + msg`There was an an issue updating your feeds, please check your internet connection and try again.`, + ), ) logger.error('Failed up update feeds', {error: err}) } - }, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed]) + }, [ + feedInfo, + isSaved, + saveFeed, + removeFeed, + resetSaveFeed, + resetRemoveFeed, + _, + ]) const onTogglePinned = React.useCallback(async () => { try { @@ -232,10 +242,10 @@ export function ProfileFeedScreenInner({ resetPinFeed() } } catch (e) { - Toast.show('There was an issue contacting the server') + Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {error: e}) } - }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed]) + }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _]) const onPressShare = React.useCallback(() => { const url = toShareUrl(feedInfo.route.href) @@ -341,7 +351,7 @@ export function ProfileFeedScreenInner({ <Button disabled={isSavePending || isRemovePending} type="default" - label={isSaved ? 'Unsave' : 'Save'} + label={isSaved ? _(msg`Unsave`) : _(msg`Save`)} onPress={onToggleSaved} style={styles.btn} /> @@ -349,7 +359,7 @@ export function ProfileFeedScreenInner({ testID={isPinned ? 'unpinBtn' : 'pinBtn'} disabled={isPinPending || isUnpinPending} type={isPinned ? 'default' : 'inverted'} - label={isPinned ? 'Unpin' : 'Pin to home'} + label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)} onPress={onTogglePinned} style={styles.btn} /> @@ -444,6 +454,7 @@ interface FeedSectionProps { } const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { + const {_} = useLingui() const [hasNew, setHasNew] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false) const queryClient = useQueryClient() @@ -470,8 +481,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( }, [onScrollToTop, isScreenFocused]) const renderPostsEmpty = useCallback(() => { - return <EmptyState icon="feed" message="This feed is empty!" /> - }, []) + return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> + }, [_]) return ( <View> @@ -479,6 +490,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( enabled={isFocused} feed={feed} pollInterval={60e3} + disablePoll={hasNew} scrollElRef={scrollElRef} onHasNew={setHasNew} onScrolledDownChange={setIsScrolledDown} @@ -488,7 +500,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( {(isScrolledDown || hasNew) && ( <LoadLatestBtn onPress={onScrollToTop} - label="Load new posts" + label={_(msg`Load new posts`)} showIndicator={hasNew} /> )} @@ -542,11 +554,13 @@ function AboutSection({ } } catch (err) { Toast.show( - 'There was an an issue contacting the server, please check your internet connection and try again.', + _( + msg`There was an an issue contacting the server, please check your internet connection and try again.`, + ), ) logger.error('Failed up toggle like', {error: err}) } - }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track]) + }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) return ( <ScrollView @@ -597,24 +611,28 @@ function AboutSection({ {typeof likeCount === 'number' && ( <TextLink href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} - text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`} + text={_( + msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`, + )} style={[pal.textLight, s.semiBold]} /> )} </View> <Text type="md" style={[pal.textLight]} numberOfLines={1}> - Created by{' '} {isOwner ? ( - 'you' + <Trans>Created by you</Trans> ) : ( - <TextLink - text={sanitizeHandle(feedInfo.creatorHandle, '@')} - href={makeProfileLink({ - did: feedInfo.creatorDid, - handle: feedInfo.creatorHandle, - })} - style={pal.textLight} - /> + <Trans> + Created by{' '} + <TextLink + text={sanitizeHandle(feedInfo.creatorHandle, '@')} + href={makeProfileLink({ + did: feedInfo.creatorDid, + handle: feedInfo.creatorHandle, + })} + style={pal.textLight} + /> + </Trans> )} </Text> </View> diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index c51758ae5..cb7962a9b 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -68,6 +68,7 @@ interface SectionRef { type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> export function ProfileListScreen(props: Props) { + const {_} = useLingui() const {name: handleOrDid, rkey} = props.route.params const {data: resolvedUri, error: resolveError} = useResolveUriQuery( AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), @@ -78,7 +79,9 @@ export function ProfileListScreen(props: Props) { return ( <CenteredView> <ErrorScreen - error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`} + error={_( + msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`, + )} /> </CenteredView> ) @@ -260,10 +263,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { await pinFeed({uri: list.uri}) } } catch (e) { - Toast.show('There was an issue contacting the server') + Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {error: e}) } - }, [list.uri, isPinned, pinFeed, unpinFeed]) + }, [list.uri, isPinned, pinFeed, unpinFeed, _]) const onSubscribeMute = useCallback(() => { openModal({ @@ -272,15 +275,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { message: _( msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, ), - confirmBtnText: 'Mute this List', + confirmBtnText: _(msg`Mute this List`), async onPressConfirm() { try { await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) - Toast.show('List muted') + Toast.show(_(msg`List muted`)) track('Lists:Mute') } catch { Toast.show( - 'There was an issue. Please check your internet connection and try again.', + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), ) } }, @@ -293,14 +298,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const onUnsubscribeMute = useCallback(async () => { try { await listMuteMutation.mutateAsync({uri: list.uri, mute: false}) - Toast.show('List unmuted') + Toast.show(_(msg`List unmuted`)) track('Lists:Unmute') } catch { Toast.show( - 'There was an issue. Please check your internet connection and try again.', + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), ) } - }, [list, listMuteMutation, track]) + }, [list, listMuteMutation, track, _]) const onSubscribeBlock = useCallback(() => { openModal({ @@ -309,15 +316,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { message: _( msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, ), - confirmBtnText: 'Block this List', + confirmBtnText: _(msg`Block this List`), async onPressConfirm() { try { await listBlockMutation.mutateAsync({uri: list.uri, block: true}) - Toast.show('List blocked') + Toast.show(_(msg`List blocked`)) track('Lists:Block') } catch { Toast.show( - 'There was an issue. Please check your internet connection and try again.', + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), ) } }, @@ -330,14 +339,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const onUnsubscribeBlock = useCallback(async () => { try { await listBlockMutation.mutateAsync({uri: list.uri, block: false}) - Toast.show('List unblocked') + Toast.show(_(msg`List unblocked`)) track('Lists:Unblock') } catch { Toast.show( - 'There was an issue. Please check your internet connection and try again.', + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), ) } - }, [list, listBlockMutation, track]) + }, [list, listBlockMutation, track, _]) const onPressEdit = useCallback(() => { openModal({ @@ -353,7 +364,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { message: _(msg`Are you sure?`), async onPressConfirm() { await listDeleteMutation.mutateAsync({uri: list.uri}) - Toast.show('List deleted') + Toast.show(_(msg`List deleted`)) track('Lists:Delete') if (navigation.canGoBack()) { navigation.goBack() @@ -545,7 +556,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { <Button testID={isPinned ? 'unpinBtn' : 'pinBtn'} type={isPinned ? 'default' : 'inverted'} - label={isPinned ? 'Unpin' : 'Pin to home'} + label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)} onPress={onTogglePinned} disabled={isPending} /> @@ -554,14 +565,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { <Button testID="unblockBtn" type="default" - label="Unblock" + label={_(msg`Unblock`)} onPress={onUnsubscribeBlock} /> ) : isMuting ? ( <Button testID="unmuteBtn" type="default" - label="Unmute" + label={_(msg`Unmute`)} onPress={onUnsubscribeMute} /> ) : ( @@ -603,6 +614,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const [hasNew, setHasNew] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false) const isScreenFocused = useIsFocused() + const {_} = useLingui() const onScrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ @@ -624,8 +636,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( }, [onScrollToTop, isScreenFocused]) const renderPostsEmpty = useCallback(() => { - return <EmptyState icon="feed" message="This feed is empty!" /> - }, []) + return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> + }, [_]) return ( <View> @@ -634,6 +646,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( enabled={isFocused} feed={feed} pollInterval={60e3} + disablePoll={hasNew} scrollElRef={scrollElRef} onHasNew={setHasNew} onScrolledDownChange={setIsScrolledDown} @@ -643,7 +656,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( {(isScrolledDown || hasNew) && ( <LoadLatestBtn onPress={onScrollToTop} - label="Load new posts" + label={_(msg`Load new posts`)} showIndicator={hasNew} /> )} @@ -721,15 +734,30 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( </Text> )} <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {isCurateList ? 'User list' : 'Moderation list'} by{' '} - {isOwner ? ( - 'you' + {isCurateList ? ( + isOwner ? ( + <Trans>User list by you</Trans> + ) : ( + <Trans> + User list by{' '} + <TextLink + text={sanitizeHandle(list.creator.handle || '', '@')} + href={makeProfileLink(list.creator)} + style={pal.textLight} + /> + </Trans> + ) + ) : isOwner ? ( + <Trans>Moderation list by you</Trans> ) : ( - <TextLink - text={sanitizeHandle(list.creator.handle || '', '@')} - href={makeProfileLink(list.creator)} - style={pal.textLight} - /> + <Trans> + Moderation list by{' '} + <TextLink + text={sanitizeHandle(list.creator.handle || '', '@')} + href={makeProfileLink(list.creator)} + style={pal.textLight} + /> + </Trans> )} </Text> </View> @@ -782,11 +810,11 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( return ( <EmptyState icon="users-slash" - message="This list is empty!" + message={_(msg`This list is empty!`)} style={{paddingTop: 40}} /> ) - }, []) + }, [_]) return ( <View> @@ -802,7 +830,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>( {isScrolledDown && ( <LoadLatestBtn onPress={onScrollToTop} - label="Scroll to top" + label={_(msg`Scroll to top`)} showIndicator={false} /> )} @@ -846,7 +874,7 @@ function ErrorScreen({error}: {error: string}) { <Button type="default" accessibilityLabel={_(msg`Go Back`)} - accessibilityHint="Return to previous page" + accessibilityHint={_(msg`Return to previous page`)} onPress={onPressBack} style={{flexShrink: 1}}> <Text type="button" style={pal.text}> diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index bbac30689..19ae37f0c 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -82,7 +82,7 @@ export function SavedFeeds({}: Props) { isTabletOrDesktop && styles.desktopContainer, ]}> <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder /> - <ScrollView style={s.flex1}> + <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}> <View style={[pal.text, pal.border, styles.title]}> <Text type="title" style={pal.text}> <Trans>Pinned Feeds</Trans> @@ -160,7 +160,7 @@ export function SavedFeeds({}: Props) { type="sm" style={pal.link} href="https://github.com/bluesky-social/feed-generator" - text="See this guide" + text={_(msg`See this guide`)} />{' '} for more information. </Trans> @@ -188,6 +188,7 @@ function ListItem({ >['reset'] }) { const pal = usePalette('default') + const {_} = useLingui() const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() const {isPending: isUnpinPending, mutateAsync: unpinFeed} = useUnpinFeedMutation() @@ -205,10 +206,10 @@ function ListItem({ await pinFeed({uri: feedUri}) } } catch (e) { - Toast.show('There was an issue contacting the server') + Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to toggle pinned feed', {error: e}) } - }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState]) + }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _]) const onPressUp = React.useCallback(async () => { if (!isPinned) return @@ -227,10 +228,10 @@ function ListItem({ index: pinned.indexOf(feedUri), }) } catch (e) { - Toast.show('There was an issue contacting the server') + Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to set pinned feed order', {error: e}) } - }, [feedUri, isPinned, setSavedFeeds, currentFeeds]) + }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _]) const onPressDown = React.useCallback(async () => { if (!isPinned) return @@ -248,10 +249,10 @@ function ListItem({ index: pinned.indexOf(feedUri), }) } catch (e) { - Toast.show('There was an issue contacting the server') + Toast.show(_(msg`There was an issue contacting the server`)) logger.error('Failed to set pinned feed order', {error: e}) } - }, [feedUri, isPinned, setSavedFeeds, currentFeeds]) + }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _]) return ( <Pressable @@ -288,7 +289,7 @@ function ListItem({ <FeedSourceCard key={feedUri} feedUri={feedUri} - style={styles.noBorder} + style={styles.noTopBorder} showSaveBtn showMinimalPlaceholder /> @@ -344,7 +345,7 @@ const styles = StyleSheet.create({ webArrowUpButton: { marginBottom: 10, }, - noBorder: { + noTopBorder: { borderTopWidth: 0, }, footerText: { @@ -352,4 +353,10 @@ const styles = StyleSheet.create({ paddingTop: 22, paddingBottom: 100, }, + noBorder: { + borderBottomWidth: 0, + borderRightWidth: 0, + borderLeftWidth: 0, + borderTopWidth: 0, + }, }) diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index b522edfba..df64cc5aa 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -42,11 +42,17 @@ import {useSetDrawerOpen} from '#/state/shell' import {useAnalytics} from '#/lib/analytics/analytics' import {MagnifyingGlassIcon} from '#/lib/icons' import {useModerationOpts} from '#/state/queries/preferences' -import {SearchResultCard} from '#/view/shell/desktop/Search' +import { + MATCH_HANDLE, + SearchLinkCard, + SearchProfileCard, +} from '#/view/shell/desktop/Search' import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' -import {isWeb} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' import {s} from '#/lib/styles' +import AsyncStorage from '@react-native-async-storage/async-storage' +import {augmentSearchQuery} from '#/lib/strings/helpers' function Loader() { const pal = usePalette('default') @@ -83,9 +89,7 @@ function EmptyState({message, error}: {message: string; error?: string}) { }, ]}> <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}> - <Text style={[pal.text]}> - <Trans>{message}</Trans> - </Text> + <Text style={[pal.text]}>{message}</Text> {error && ( <> @@ -162,6 +166,8 @@ function SearchScreenSuggestedFollows() { // @ts-ignore web only -prf desktopFixedHeight contentContainerStyle={{paddingBottom: 1200}} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" /> ) : ( <CenteredView sideBorders style={[pal.border, s.hContentRegion]}> @@ -303,13 +309,23 @@ function SearchScreenUserResults({query}: {query: string}) { const SECTIONS_LOGGEDOUT = ['Users'] const SECTIONS_LOGGEDIN = ['Posts', 'Users'] -export function SearchScreenInner({query}: {query?: string}) { +export function SearchScreenInner({ + query, + primarySearch, +}: { + query?: string + primarySearch?: boolean +}) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() - const {hasSession} = useSession() + const {hasSession, currentAccount} = useSession() const {isDesktop} = useWebMediaQueries() + const augmentedQuery = React.useMemo(() => { + return augmentSearchQuery(query || '', {did: currentAccount?.did}) + }, [query, currentAccount]) + const onPageSelected = React.useCallback( (index: number) => { setMinimalShellMode(false) @@ -324,13 +340,15 @@ export function SearchScreenInner({query}: {query?: string}) { tabBarPosition="top" onPageSelected={onPageSelected} renderTabBar={props => ( - <CenteredView sideBorders style={pal.border}> + <CenteredView + sideBorders + style={[pal.border, pal.view, styles.tabBarContainer]}> <TabBar items={SECTIONS_LOGGEDIN} {...props} /> </CenteredView> )} initialPage={0}> <View> - <SearchScreenPostResults query={query} /> + <SearchScreenPostResults query={augmentedQuery} /> </View> <View> <SearchScreenUserResults query={query} /> @@ -365,7 +383,9 @@ export function SearchScreenInner({query}: {query?: string}) { tabBarPosition="top" onPageSelected={onPageSelected} renderTabBar={props => ( - <CenteredView sideBorders style={pal.border}> + <CenteredView + sideBorders + style={[pal.border, pal.view, styles.tabBarContainer]}> <TabBar items={SECTIONS_LOGGEDOUT} {...props} /> </CenteredView> )} @@ -413,7 +433,7 @@ export function SearchScreenInner({query}: {query?: string}) { style={pal.textLight} /> <Text type="xl" style={[pal.textLight, {paddingHorizontal: 18}]}> - {isDesktop ? ( + {isDesktop && !primarySearch ? ( <Trans>Find users with the search tool on the right</Trans> ) : ( <Trans>Find users on Bluesky</Trans> @@ -425,19 +445,7 @@ export function SearchScreenInner({query}: {query?: string}) { ) } -export function SearchScreenDesktop( - props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, -) { - const {isDesktop} = useWebMediaQueries() - - return isDesktop ? ( - <SearchScreenInner query={props.route.params?.q} /> - ) : ( - <SearchScreenMobile {...props} /> - ) -} - -export function SearchScreenMobile( +export function SearchScreen( props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>, ) { const theme = useTheme() @@ -449,7 +457,7 @@ export function SearchScreenMobile( const moderationOpts = useModerationOpts() const search = useActorAutocompleteFn() const setMinimalShellMode = useSetMinimalShellMode() - const {isTablet} = useWebMediaQueries() + const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries() const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( undefined, @@ -462,32 +470,56 @@ export function SearchScreenMobile( const [inputIsFocused, setInputIsFocused] = React.useState(false) const [showAutocompleteResults, setShowAutocompleteResults] = React.useState(false) + const [searchHistory, setSearchHistory] = React.useState<string[]>([]) + + React.useEffect(() => { + const loadSearchHistory = async () => { + try { + const history = await AsyncStorage.getItem('searchHistory') + if (history !== null) { + setSearchHistory(JSON.parse(history)) + } + } catch (e: any) { + logger.error('Failed to load search history', e) + } + } + + loadSearchHistory() + }, []) const onPressMenu = React.useCallback(() => { track('ViewHeader:MenuButtonClicked') setDrawerOpen(true) }, [track, setDrawerOpen]) + const onPressCancelSearch = React.useCallback(() => { + scrollToTopWeb() textInput.current?.blur() setQuery('') setShowAutocompleteResults(false) if (searchDebounceTimeout.current) clearTimeout(searchDebounceTimeout.current) }, [textInput]) + const onPressClearQuery = React.useCallback(() => { + scrollToTopWeb() setQuery('') setShowAutocompleteResults(false) }, [setQuery]) + const onChangeText = React.useCallback( async (text: string) => { + scrollToTopWeb() + setQuery(text) if (text.length > 0) { setIsFetching(true) setShowAutocompleteResults(true) - if (searchDebounceTimeout.current) + if (searchDebounceTimeout.current) { clearTimeout(searchDebounceTimeout.current) + } searchDebounceTimeout.current = setTimeout(async () => { const results = await search({query: text, limit: 30}) @@ -498,8 +530,9 @@ export function SearchScreenMobile( } }, 300) } else { - if (searchDebounceTimeout.current) + if (searchDebounceTimeout.current) { clearTimeout(searchDebounceTimeout.current) + } setSearchResults([]) setIsFetching(false) setShowAutocompleteResults(false) @@ -507,14 +540,47 @@ export function SearchScreenMobile( }, [setQuery, search, setSearchResults], ) + + const updateSearchHistory = React.useCallback( + async (newQuery: string) => { + newQuery = newQuery.trim() + if (newQuery && !searchHistory.includes(newQuery)) { + let newHistory = [newQuery, ...searchHistory] + + if (newHistory.length > 5) { + newHistory = newHistory.slice(0, 5) + } + + setSearchHistory(newHistory) + try { + await AsyncStorage.setItem( + 'searchHistory', + JSON.stringify(newHistory), + ) + } catch (e: any) { + logger.error('Failed to save search history', e) + } + } + }, + [searchHistory, setSearchHistory], + ) + const onSubmit = React.useCallback(() => { + scrollToTopWeb() setShowAutocompleteResults(false) - }, [setShowAutocompleteResults]) + updateSearchHistory(query) + }, [query, setShowAutocompleteResults, updateSearchHistory]) const onSoftReset = React.useCallback(() => { + scrollToTopWeb() onPressCancelSearch() }, [onPressCancelSearch]) + const queryMaybeHandle = React.useMemo(() => { + const match = MATCH_HANDLE.exec(query) + return match && match[1] + }, [query]) + useFocusEffect( React.useCallback(() => { setMinimalShellMode(false) @@ -522,19 +588,47 @@ export function SearchScreenMobile( }, [onSoftReset, setMinimalShellMode]), ) + const handleHistoryItemClick = (item: React.SetStateAction<string>) => { + setQuery(item) + onSubmit() + } + + const handleRemoveHistoryItem = (itemToRemove: string) => { + const updatedHistory = searchHistory.filter(item => item !== itemToRemove) + setSearchHistory(updatedHistory) + AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch( + e => { + logger.error('Failed to update search history', e) + }, + ) + } + return ( - <View style={{flex: 1}}> - <CenteredView style={[styles.header, pal.border]} sideBorders={isTablet}> - <Pressable - testID="viewHeaderBackOrMenuBtn" - onPress={onPressMenu} - hitSlop={HITSLOP_10} - style={styles.headerMenuBtn} - accessibilityRole="button" - accessibilityLabel={_(msg`Menu`)} - accessibilityHint="Access navigation links and settings"> - <FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} /> - </Pressable> + <View style={isWeb ? null : {flex: 1}}> + <CenteredView + style={[ + styles.header, + pal.border, + pal.view, + isTabletOrDesktop && {paddingTop: 10}, + ]} + sideBorders={isTabletOrDesktop}> + {isTabletOrMobile && ( + <Pressable + testID="viewHeaderBackOrMenuBtn" + onPress={onPressMenu} + hitSlop={HITSLOP_10} + style={styles.headerMenuBtn} + accessibilityRole="button" + accessibilityLabel={_(msg`Menu`)} + accessibilityHint={_(msg`Access navigation links and settings`)}> + <FontAwesomeIcon + icon="bars" + size={18} + color={pal.colors.textLight} + /> + </Pressable> + )} <View style={[ @@ -548,7 +642,7 @@ export function SearchScreenMobile( <TextInput testID="searchTextInput" ref={textInput} - placeholder="Search" + placeholder={_(msg`Search`)} placeholderTextColor={pal.colors.textLight} selectTextOnFocus returnKeyType="search" @@ -556,7 +650,12 @@ export function SearchScreenMobile( style={[pal.text, styles.headerSearchInput]} keyboardAppearance={theme.colorScheme} onFocus={() => setInputIsFocused(true)} - onBlur={() => setInputIsFocused(false)} + onBlur={() => { + // HACK + // give 100ms to not stop click handlers in the search history + // -prf + setTimeout(() => setInputIsFocused(false), 100) + }} onChangeText={onChangeText} onSubmitEditing={onSubmit} autoFocus={false} @@ -564,6 +663,7 @@ export function SearchScreenMobile( accessibilityLabel={_(msg`Search`)} accessibilityHint="" autoCorrect={false} + autoComplete="off" autoCapitalize="none" /> {query ? ( @@ -572,7 +672,8 @@ export function SearchScreenMobile( onPress={onPressClearQuery} accessibilityRole="button" accessibilityLabel={_(msg`Clear search query`)} - accessibilityHint=""> + accessibilityHint="" + hitSlop={HITSLOP_10}> <FontAwesomeIcon icon="xmark" size={16} @@ -584,7 +685,10 @@ export function SearchScreenMobile( {query || inputIsFocused ? ( <View style={styles.headerCancelBtn}> - <Pressable onPress={onPressCancelSearch} accessibilityRole="button"> + <Pressable + onPress={onPressCancelSearch} + accessibilityRole="button" + hitSlop={HITSLOP_10}> <Text style={[pal.text]}> <Trans>Cancel</Trans> </Text> @@ -593,29 +697,83 @@ export function SearchScreenMobile( ) : undefined} </CenteredView> - {showAutocompleteResults && moderationOpts ? ( + {showAutocompleteResults ? ( <> - {isFetching ? ( + {isFetching || !moderationOpts ? ( <Loader /> ) : ( - <ScrollView style={{height: '100%'}}> - {searchResults.length ? ( - searchResults.map((item, i) => ( - <SearchResultCard - key={item.did} - profile={item} - moderation={moderateProfile(item, moderationOpts)} - style={i === 0 ? {borderTopWidth: 0} : {}} - /> - )) - ) : ( - <EmptyState message={_(msg`No results found for ${query}`)} /> - )} + <ScrollView + style={{height: '100%'}} + // @ts-ignore web only -prf + dataSet={{stableGutters: '1'}} + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag"> + <SearchLinkCard + label={_(msg`Search for "${query}"`)} + onPress={isNative ? onSubmit : undefined} + to={ + isNative + ? undefined + : `/search?q=${encodeURIComponent(query)}` + } + style={{borderBottomWidth: 1}} + /> + + {queryMaybeHandle ? ( + <SearchLinkCard + label={_(msg`Go to @${queryMaybeHandle}`)} + to={`/profile/${queryMaybeHandle}`} + /> + ) : null} + + {searchResults.map(item => ( + <SearchProfileCard + key={item.did} + profile={item} + moderation={moderateProfile(item, moderationOpts)} + /> + ))} <View style={{height: 200}} /> </ScrollView> )} </> + ) : !query && inputIsFocused ? ( + <CenteredView + sideBorders={isTabletOrDesktop} + // @ts-ignore web only -prf + style={{ + height: isWeb ? '100vh' : undefined, + }}> + <View style={styles.searchHistoryContainer}> + {searchHistory.length > 0 && ( + <View style={styles.searchHistoryContent}> + <Text style={[pal.text, styles.searchHistoryTitle]}> + Recent Searches + </Text> + {searchHistory.map((historyItem, index) => ( + <View key={index} style={styles.historyItemContainer}> + <Pressable + accessibilityRole="button" + onPress={() => handleHistoryItemClick(historyItem)} + style={styles.historyItem}> + <Text style={pal.text}>{historyItem}</Text> + </Pressable> + <Pressable + accessibilityRole="button" + onPress={() => handleRemoveHistoryItem(historyItem)}> + <FontAwesomeIcon + icon="xmark" + size={16} + style={pal.textLight as FontAwesomeIconStyle} + /> + </Pressable> + </View> + ))} + </View> + )} + </View> + </CenteredView> ) : ( <SearchScreenInner query={query} /> )} @@ -623,12 +781,25 @@ export function SearchScreenMobile( ) } +function scrollToTopWeb() { + if (isWeb) { + window.scrollTo(0, 0) + } +} + +const HEADER_HEIGHT = 50 + const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 4, + height: HEADER_HEIGHT, + // @ts-ignore web only + position: isWeb ? 'sticky' : '', + top: 0, + zIndex: 1, }, headerMenuBtn: { width: 30, @@ -658,4 +829,30 @@ const styles = StyleSheet.create({ headerCancelBtn: { paddingLeft: 10, }, + tabBarContainer: { + // @ts-ignore web only + position: isWeb ? 'sticky' : '', + top: isWeb ? HEADER_HEIGHT : 0, + zIndex: 1, + }, + searchHistoryContainer: { + width: '100%', + paddingHorizontal: 12, + }, + searchHistoryContent: { + padding: 10, + borderRadius: 8, + }, + searchHistoryTitle: { + fontWeight: 'bold', + }, + historyItem: { + paddingVertical: 8, + }, + historyItemContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 8, + }, }) diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx index a65149bf7..f6c0eca26 100644 --- a/src/view/screens/Search/index.tsx +++ b/src/view/screens/Search/index.tsx @@ -1,3 +1 @@ -import {SearchScreenMobile} from '#/view/screens/Search/Search' - -export const SearchScreen = SearchScreenMobile +export {SearchScreen} from '#/view/screens/Search/Search' diff --git a/src/view/screens/Search/index.web.tsx b/src/view/screens/Search/index.web.tsx deleted file mode 100644 index 8e039e3cd..000000000 --- a/src/view/screens/Search/index.web.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import {SearchScreenDesktop} from '#/view/screens/Search/Search' - -export const SearchScreen = SearchScreenDesktop diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index d48112dae..3b50c5449 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -19,7 +19,6 @@ import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import * as AppInfo from 'lib/app-info' import {s, colors} from 'lib/styles' import {ScrollView} from '../com/util/Views' -import {ViewHeader} from '../com/util/ViewHeader' import {Link, TextLink} from '../com/util/Link' import {Text} from '../com/util/text/Text' import * as Toast from '../com/util/Toast' @@ -36,6 +35,7 @@ import {HandIcon, HashtagIcon} from 'lib/icons' import Clipboard from '@react-native-clipboard/clipboard' import {makeProfileLink} from 'lib/routes/links' import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' +import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {useModalControls} from '#/state/modals' import { @@ -70,9 +70,15 @@ import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' +import { + useInAppBrowser, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' +import {isNative} from '#/platform/detection' function SettingsAccountCard({account}: {account: SessionAccount}) { const pal = usePalette('default') + const {_} = useLingui() const {isSwitchingAccounts, currentAccount} = useSession() const {logout} = useSessionApi() const {data: profile} = useProfileQuery({did: account.did}) @@ -98,10 +104,10 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { testID="signOutBtn" onPress={logout} accessibilityRole="button" - accessibilityLabel="Sign out" + accessibilityLabel={_(msg`Sign out`)} accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> <Text type="lg" style={pal.link}> - Sign out + <Trans>Sign out</Trans> </Text> </TouchableOpacity> ) : ( @@ -116,7 +122,7 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { did: currentAccount?.did, handle: currentAccount?.handle, })} - title="Your profile" + title={_(msg`Your profile`)} noFeedback> {contents} </Link> @@ -128,8 +134,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) { isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) } accessibilityRole="button" - accessibilityLabel={`Switch to ${account.handle}`} - accessibilityHint="Switches the account you are logged in to"> + accessibilityLabel={_(msg`Switch to ${account.handle}`)} + accessibilityHint={_(msg`Switches the account you are logged in to`)}> {contents} </TouchableOpacity> ) @@ -145,6 +151,8 @@ export function SettingsScreen({}: Props) { const setMinimalShellMode = useSetMinimalShellMode() const requireAltTextEnabled = useRequireAltTextEnabled() const setRequireAltTextEnabled = useSetRequireAltTextEnabled() + const inAppBrowserPref = useInAppBrowser() + const setUseInAppBrowser = useSetInAppBrowser() const onboardingDispatch = useOnboardingDispatch() const navigation = useNavigation<NavigationProp>() const {isMobile} = useWebMediaQueries() @@ -225,15 +233,15 @@ export function SettingsScreen({}: Props) { const onPressResetOnboarding = React.useCallback(async () => { onboardingDispatch({type: 'start'}) - Toast.show('Onboarding reset') - }, [onboardingDispatch]) + Toast.show(_(msg`Onboarding reset`)) + }, [onboardingDispatch, _]) const onPressBuildInfo = React.useCallback(() => { Clipboard.setString( `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, ) - Toast.show('Copied build version to clipboard') - }, []) + Toast.show(_(msg`Copied build version to clipboard`)) + }, [_]) const openHomeFeedPreferences = React.useCallback(() => { navigation.navigate('PreferencesHomeFeed') @@ -265,20 +273,34 @@ export function SettingsScreen({}: Props) { const clearAllStorage = React.useCallback(async () => { await clearStorage() - Toast.show(`Storage cleared, you need to restart the app now.`) - }, []) + Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) + }, [_]) const clearAllLegacyStorage = React.useCallback(async () => { await clearLegacyStorage() - Toast.show(`Legacy storage cleared, you need to restart the app now.`) - }, []) + Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`)) + }, [_]) return ( - <View style={[s.hContentRegion]} testID="settingsScreen"> - <ViewHeader title={_(msg`Settings`)} /> + <View style={s.hContentRegion} testID="settingsScreen"> + <SimpleViewHeader + showBackButton={isMobile} + style={[ + pal.border, + {borderBottomWidth: 1}, + !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, + ]}> + <View style={{flex: 1}}> + <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}> + <Trans>Settings</Trans> + </Text> + </View> + </SimpleViewHeader> <ScrollView style={[s.hContentRegion]} contentContainerStyle={isMobile && pal.viewLight} - scrollIndicatorInsets={{right: 1}}> + scrollIndicatorInsets={{right: 1}} + // @ts-ignore web only -prf + dataSet={{'stable-gutters': 1}}> <View style={styles.spacer20} /> {currentAccount ? ( <> @@ -298,12 +320,18 @@ export function SettingsScreen({}: Props) { /> </> )} - <Text type="lg" style={pal.text}> - {currentAccount.email || '(no email)'}{' '} + <Text + type="lg" + numberOfLines={1} + style={[ + pal.text, + {overflow: 'hidden', marginRight: 4, flex: 1}, + ]}> + {currentAccount.email || '(no email)'} </Text> <Link onPress={() => openModal({name: 'change-email'})}> <Text type="lg" style={pal.link}> - <Trans>Change</Trans> + <Trans context="action">Change</Trans> </Text> </Link> </View> @@ -353,7 +381,7 @@ export function SettingsScreen({}: Props) { onPress={isSwitchingAccounts ? undefined : onPressAddAccount} accessibilityRole="button" accessibilityLabel={_(msg`Add account`)} - accessibilityHint="Create a new Bluesky account"> + accessibilityHint={_(msg`Create a new Bluesky account`)}> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="plus" @@ -381,7 +409,7 @@ export function SettingsScreen({}: Props) { onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} accessibilityRole="button" accessibilityLabel={_(msg`Invite`)} - accessibilityHint="Opens invite code list" + accessibilityHint={_(msg`Opens invite code list`)} disabled={invites?.disabled}> <View style={[ @@ -419,7 +447,7 @@ export function SettingsScreen({}: Props) { <View style={[pal.view, styles.toggleCard]}> <ToggleButton type="default-light" - label="Require alt text before posting" + label={_(msg`Require alt text before posting`)} labelType="lg" isSelected={requireAltTextEnabled} onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} @@ -435,23 +463,23 @@ export function SettingsScreen({}: Props) { <View style={[styles.linkCard, pal.view, styles.selectableBtns]}> <SelectableBtn selected={colorMode === 'system'} - label="System" + label={_(msg`System`)} left onSelect={() => setColorMode('system')} - accessibilityHint="Set color theme to system setting" + accessibilityHint={_(msg`Set color theme to system setting`)} /> <SelectableBtn selected={colorMode === 'light'} - label="Light" + label={_(msg`Light`)} onSelect={() => setColorMode('light')} - accessibilityHint="Set color theme to light" + accessibilityHint={_(msg`Set color theme to light`)} /> <SelectableBtn selected={colorMode === 'dark'} - label="Dark" + label={_(msg`Dark`)} right onSelect={() => setColorMode('dark')} - accessibilityHint="Set color theme to dark" + accessibilityHint={_(msg`Set color theme to dark`)} /> </View> </View> @@ -529,8 +557,8 @@ export function SettingsScreen({}: Props) { ]} onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} accessibilityRole="button" - accessibilityHint="Language settings" - accessibilityLabel={_(msg`Opens configurable language settings`)}> + accessibilityLabel={_(msg`Language settings`)} + accessibilityHint={_(msg`Opens configurable language settings`)}> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="language" @@ -554,8 +582,8 @@ export function SettingsScreen({}: Props) { : () => navigation.navigate('Moderation') } accessibilityRole="button" - accessibilityHint="" - accessibilityLabel={_(msg`Opens moderation settings`)}> + accessibilityLabel={_(msg`Moderation settings`)} + accessibilityHint={_(msg`Opens moderation settings`)}> <View style={[styles.iconContainer, pal.btn]}> <HandIcon style={pal.text} size={18} strokeWidth={6} /> </View> @@ -563,6 +591,39 @@ export function SettingsScreen({}: Props) { <Trans>Moderation</Trans> </Text> </TouchableOpacity> + + <View style={styles.spacer20} /> + + <Text type="xl-bold" style={[pal.text, styles.heading]}> + <Trans>Privacy</Trans> + </Text> + + <TouchableOpacity + testID="externalEmbedsBtn" + style={[ + styles.linkCard, + pal.view, + isSwitchingAccounts && styles.dimmed, + ]} + onPress={ + isSwitchingAccounts + ? undefined + : () => navigation.navigate('PreferencesExternalEmbeds') + } + accessibilityRole="button" + accessibilityLabel={_(msg`External media settings`)} + accessibilityHint={_(msg`Opens external embeds settings`)}> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon={['far', 'circle-play']} + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + <Trans>External Media Preferences</Trans> + </Text> + </TouchableOpacity> + <View style={styles.spacer20} /> <Text type="xl-bold" style={[pal.text, styles.heading]}> @@ -577,8 +638,8 @@ export function SettingsScreen({}: Props) { ]} onPress={onPressAppPasswords} accessibilityRole="button" - accessibilityHint="Open app password settings" - accessibilityLabel={_(msg`Opens the app password settings page`)}> + accessibilityLabel={_(msg`App password settings`)} + accessibilityHint={_(msg`Opens the app password settings page`)}> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="lock" @@ -599,7 +660,7 @@ export function SettingsScreen({}: Props) { onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} accessibilityRole="button" accessibilityLabel={_(msg`Change handle`)} - accessibilityHint="Choose a new Bluesky username or create"> + accessibilityHint={_(msg`Choose a new Bluesky username or create`)}> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="at" @@ -610,6 +671,17 @@ export function SettingsScreen({}: Props) { <Trans>Change handle</Trans> </Text> </TouchableOpacity> + {isNative && ( + <View style={[pal.view, styles.toggleCard]}> + <ToggleButton + type="default-light" + label={_(msg`Open links with in-app browser`)} + labelType="lg" + isSelected={inAppBrowserPref ?? false} + onPress={() => setUseInAppBrowser(!inAppBrowserPref)} + /> + </View> + )} <View style={styles.spacer20} /> <Text type="xl-bold" style={[pal.text, styles.heading]}> <Trans>Danger Zone</Trans> @@ -620,7 +692,9 @@ export function SettingsScreen({}: Props) { accessible={true} accessibilityRole="button" accessibilityLabel={_(msg`Delete account`)} - accessibilityHint="Opens modal for account deletion confirmation. Requires email code."> + accessibilityHint={_( + msg`Opens modal for account deletion confirmation. Requires email code.`, + )}> <View style={[styles.iconContainer, dangerBg]}> <FontAwesomeIcon icon={['far', 'trash-can']} @@ -660,8 +734,8 @@ export function SettingsScreen({}: Props) { style={[pal.view, styles.linkCardNoIcon]} onPress={onPressStorybook} accessibilityRole="button" - accessibilityHint="Open storybook page" - accessibilityLabel={_(msg`Opens the storybook page`)}> + accessibilityLabel={_(msg`Open storybook page`)} + accessibilityHint={_(msg`Opens the storybook page`)}> <Text type="lg" style={pal.text}> <Trans>Storybook</Trans> </Text> @@ -670,8 +744,8 @@ export function SettingsScreen({}: Props) { style={[pal.view, styles.linkCardNoIcon]} onPress={onPressResetPreferences} accessibilityRole="button" - accessibilityHint="Reset preferences" - accessibilityLabel={_(msg`Resets the preferences state`)}> + accessibilityLabel={_(msg`Reset preferences`)} + accessibilityHint={_(msg`Resets the preferences state`)}> <Text type="lg" style={pal.text}> <Trans>Reset preferences state</Trans> </Text> @@ -680,8 +754,8 @@ export function SettingsScreen({}: Props) { style={[pal.view, styles.linkCardNoIcon]} onPress={onPressResetOnboarding} accessibilityRole="button" - accessibilityHint="Reset onboarding" - accessibilityLabel={_(msg`Resets the onboarding state`)}> + accessibilityLabel={_(msg`Reset onboarding`)} + accessibilityHint={_(msg`Resets the onboarding state`)}> <Text type="lg" style={pal.text}> <Trans>Reset onboarding state</Trans> </Text> @@ -690,8 +764,8 @@ export function SettingsScreen({}: Props) { style={[pal.view, styles.linkCardNoIcon]} onPress={clearAllLegacyStorage} accessibilityRole="button" - accessibilityHint="Clear all legacy storage data" - accessibilityLabel={_(msg`Clear all legacy storage data`)}> + accessibilityLabel={_(msg`Clear all legacy storage data`)} + accessibilityHint={_(msg`Clear all legacy storage data`)}> <Text type="lg" style={pal.text}> <Trans> Clear all legacy storage data (restart after this) @@ -702,8 +776,8 @@ export function SettingsScreen({}: Props) { style={[pal.view, styles.linkCardNoIcon]} onPress={clearAllStorage} accessibilityRole="button" - accessibilityHint="Clear all storage data" - accessibilityLabel={_(msg`Clear all storage data`)}> + accessibilityLabel={_(msg`Clear all storage data`)} + accessibilityHint={_(msg`Clear all storage data`)}> <Text type="lg" style={pal.text}> <Trans>Clear all storage data (restart after this)</Trans> </Text> diff --git a/src/view/screens/Storybook/Breakpoints.tsx b/src/view/screens/Storybook/Breakpoints.tsx new file mode 100644 index 000000000..1b846d517 --- /dev/null +++ b/src/view/screens/Storybook/Breakpoints.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme, useBreakpoints} from '#/alf' +import {Text, H3} from '#/components/Typography' + +export function Breakpoints() { + const t = useTheme() + const breakpoints = useBreakpoints() + + return ( + <View> + <H3 style={[a.pb_md]}>Breakpoint Debugger</H3> + <Text style={[a.pb_md]}> + Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>} + {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>} + {breakpoints.gtTablet && <Text>desktop</Text>} + </Text> + <Text + style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}> + {JSON.stringify(breakpoints, null, 2)} + </Text> + </View> + ) +} diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx new file mode 100644 index 000000000..fbdc84eb4 --- /dev/null +++ b/src/view/screens/Storybook/Buttons.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import { + Button, + ButtonVariant, + ButtonColor, + ButtonIcon, + ButtonText, +} from '#/components/Button' +import {H1} from '#/components/Typography' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' + +export function Buttons() { + return ( + <View style={[a.gap_md]}> + <H1>Buttons</H1> + + <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}> + {['primary', 'secondary', 'negative'].map(color => ( + <View key={color} style={[a.gap_md, a.align_start]}> + {['solid', 'outline', 'ghost'].map(variant => ( + <React.Fragment key={variant}> + <Button + variant={variant as ButtonVariant} + color={color as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant={variant as ButtonVariant} + color={color as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ))} + </View> + ))} + + <View style={[a.flex_row, a.gap_md, a.align_start]}> + <View style={[a.gap_md, a.align_start]}> + {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map( + name => ( + <React.Fragment key={name}> + <Button + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ), + )} + </View> + <View style={[a.gap_md, a.align_start]}> + {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map( + name => ( + <React.Fragment key={name}> + <Button + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + <Button + disabled + variant="gradient" + color={name as ButtonColor} + size="large" + label="Click here"> + Button + </Button> + </React.Fragment> + ), + )} + </View> + </View> + + <Button + variant="gradient" + color="gradient_sky" + size="large" + label="Link out"> + <ButtonText>Link out</ButtonText> + <ButtonIcon icon={ArrowTopRight} /> + </Button> + + <Button + variant="gradient" + color="gradient_sky" + size="small" + label="Link out"> + <ButtonText>Link out</ButtonText> + <ButtonIcon icon={ArrowTopRight} /> + </Button> + + <Button + variant="gradient" + color="gradient_sky" + size="small" + label="Link out"> + <ButtonIcon icon={Globe} /> + <ButtonText>See the world</ButtonText> + </Button> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx new file mode 100644 index 000000000..db568c6bd --- /dev/null +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Button} from '#/components/Button' +import {H3, P} from '#/components/Typography' +import * as Dialog from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' +import {useDialogStateControlContext} from '#/state/dialogs' + +export function Dialogs() { + const control = Dialog.useDialogControl() + const prompt = Prompt.usePromptControl() + const {closeAllDialogs} = useDialogStateControlContext() + + return ( + <View style={[a.gap_md]}> + <Button + variant="outline" + color="secondary" + size="small" + onPress={() => { + control.open() + prompt.open() + }} + label="Open basic dialog"> + Open basic dialog + </Button> + + <Button + variant="solid" + color="primary" + size="small" + onPress={() => prompt.open()} + label="Open prompt"> + Open prompt + </Button> + + <Prompt.Outer control={prompt}> + <Prompt.Title>This is a prompt</Prompt.Title> + <Prompt.Description> + This is a generic prompt component. It accepts a title and a + description, as well as two actions. + </Prompt.Description> + <Prompt.Actions> + <Prompt.Cancel>Cancel</Prompt.Cancel> + <Prompt.Action>Confirm</Prompt.Action> + </Prompt.Actions> + </Prompt.Outer> + + <Dialog.Outer + control={control} + nativeOptions={{sheet: {snapPoints: ['90%']}}}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title"> + <View style={[a.relative, a.gap_md, a.w_full]}> + <H3 nativeID="dialog-title">Dialog</H3> + <P nativeID="dialog-description"> + A scrollable dialog with an input within it. + </P> + <Dialog.Input value="" onChangeText={() => {}} label="Type here" /> + + <Button + variant="outline" + color="secondary" + size="small" + onPress={closeAllDialogs} + label="Close all dialogs"> + Close all dialogs + </Button> + <View style={{height: 1000}} /> + <View style={[a.flex_row, a.justify_end]}> + <Button + variant="outline" + color="primary" + size="small" + onPress={() => control.close()} + label="Open basic dialog"> + Close basic dialog + </Button> + </View> + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + </View> + ) +} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx new file mode 100644 index 000000000..9396cca67 --- /dev/null +++ b/src/view/screens/Storybook/Forms.tsx @@ -0,0 +1,215 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {H1, H3} from '#/components/Typography' +import * as TextField from '#/components/forms/TextField' +import {DateField, Label} from '#/components/forms/DateField' +import * as Toggle from '#/components/forms/Toggle' +import * as ToggleButton from '#/components/forms/ToggleButton' +import {Button} from '#/components/Button' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' + +export function Forms() { + const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) + const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b']) + const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b']) + const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn']) + + const [value, setValue] = React.useState('') + const [date, setDate] = React.useState('2001-01-01') + + return ( + <View style={[a.gap_4xl, a.align_start]}> + <H1>Forms</H1> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>InputText</H3> + + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + + <TextField.Root> + <TextField.Icon icon={Globe} /> + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + </TextField.Root> + + <View style={[a.w_full]}> + <TextField.Label>Text field</TextField.Label> + <TextField.Root> + <TextField.Icon icon={Globe} /> + <TextField.Input + value={value} + onChangeText={setValue} + label="Text field" + /> + <TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix> + </TextField.Root> + </View> + + <View style={[a.w_full]}> + <TextField.Label>Textarea</TextField.Label> + <TextField.Input + multiline + numberOfLines={4} + value={value} + onChangeText={setValue} + label="Text field" + /> + </View> + + <H3>DateField</H3> + + <View style={[a.w_full]}> + <Label>Date</Label> + <DateField + testID="date" + value={date} + onChangeDate={date => { + console.log(date) + setDate(date) + }} + label="Input" + /> + </View> + </View> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>Toggles</H3> + + <Toggle.Item name="a" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Uncontrolled toggle</Toggle.Label> + </Toggle.Item> + + <Toggle.Group + label="Toggle" + type="checkbox" + maxSelections={2} + values={toggleGroupAValues} + onChange={setToggleGroupAValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Switch /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + + <Toggle.Group + label="Toggle" + type="checkbox" + maxSelections={2} + values={toggleGroupBValues} + onChange={setToggleGroupBValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Checkbox /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + + <Toggle.Group + label="Toggle" + type="radio" + values={toggleGroupCValues} + onChange={setToggleGroupCValues}> + <View style={[a.gap_md]}> + <Toggle.Item name="a" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="b" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="c" label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="d" disabled label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + <Toggle.Item name="e" isInvalid label="Click me"> + <Toggle.Radio /> + <Toggle.Label>Click me</Toggle.Label> + </Toggle.Item> + </View> + </Toggle.Group> + </View> + + <Button + variant="gradient" + color="gradient_nordic" + size="small" + label="Reset all toggles" + onPress={() => { + setToggleGroupAValues(['a']) + setToggleGroupBValues(['a', 'b']) + setToggleGroupCValues(['a']) + }}> + Reset all toggles + </Button> + + <View style={[a.gap_md, a.align_start, a.w_full]}> + <H3>ToggleButton</H3> + + <ToggleButton.Group + label="Preferences" + values={toggleGroupDValues} + onChange={setToggleGroupDValues}> + <ToggleButton.Button name="hide" label="Hide"> + Hide + </ToggleButton.Button> + <ToggleButton.Button name="warn" label="Warn"> + Warn + </ToggleButton.Button> + <ToggleButton.Button name="show" label="Show"> + Show + </ToggleButton.Button> + </ToggleButton.Group> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx new file mode 100644 index 000000000..73466e077 --- /dev/null +++ b/src/view/screens/Storybook/Icons.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {H1} from '#/components/Typography' +import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' +import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight' +import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' + +export function Icons() { + const t = useTheme() + return ( + <View style={[a.gap_md]}> + <H1>Icons</H1> + + <View style={[a.flex_row, a.gap_xl]}> + <Globe size="xs" fill={t.atoms.text.color} /> + <Globe size="sm" fill={t.atoms.text.color} /> + <Globe size="md" fill={t.atoms.text.color} /> + <Globe size="lg" fill={t.atoms.text.color} /> + <Globe size="xl" fill={t.atoms.text.color} /> + </View> + + <View style={[a.flex_row, a.gap_xl]}> + <ArrowTopRight size="xs" fill={t.atoms.text.color} /> + <ArrowTopRight size="sm" fill={t.atoms.text.color} /> + <ArrowTopRight size="md" fill={t.atoms.text.color} /> + <ArrowTopRight size="lg" fill={t.atoms.text.color} /> + <ArrowTopRight size="xl" fill={t.atoms.text.color} /> + </View> + + <View style={[a.flex_row, a.gap_xl]}> + <CalendarDays size="xs" fill={t.atoms.text.color} /> + <CalendarDays size="sm" fill={t.atoms.text.color} /> + <CalendarDays size="md" fill={t.atoms.text.color} /> + <CalendarDays size="lg" fill={t.atoms.text.color} /> + <CalendarDays size="xl" fill={t.atoms.text.color} /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx new file mode 100644 index 000000000..c3b1c0e0f --- /dev/null +++ b/src/view/screens/Storybook/Links.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {ButtonText} from '#/components/Button' +import {Link} from '#/components/Link' +import {H1, H3} from '#/components/Typography' + +export function Links() { + return ( + <View style={[a.gap_md, a.align_start]}> + <H1>Links</H1> + + <View style={[a.gap_md, a.align_start]}> + <Link + to="https://blueskyweb.xyz" + warnOnMismatchingTextChild + style={[a.text_md]}> + External + </Link> + <Link to="https://blueskyweb.xyz" style={[a.text_md]}> + <H3>External with custom children</H3> + </Link> + <Link + to="https://blueskyweb.xyz" + warnOnMismatchingTextChild + style={[a.text_lg]}> + https://blueskyweb.xyz + </Link> + <Link + to="https://bsky.app/profile/bsky.app" + warnOnMismatchingTextChild + style={[a.text_md]}> + Internal + </Link> + + <Link + variant="solid" + color="primary" + size="large" + label="View @bsky.app's profile" + to="https://bsky.app/profile/bsky.app"> + <ButtonText>Link as a button</ButtonText> + </Link> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx new file mode 100644 index 000000000..b521fe860 --- /dev/null +++ b/src/view/screens/Storybook/Palette.tsx @@ -0,0 +1,336 @@ +import React from 'react' +import {View} from 'react-native' + +import * as tokens from '#/alf/tokens' +import {atoms as a} from '#/alf' + +export function Palette() { + return ( + <View style={[a.gap_md]}> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_975}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.gray_1000}, + ]} + /> + </View> + + <View style={[a.flex_row, a.gap_md]}> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.blue_975}, + ]} + /> + </View> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_25}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_50}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.green_975}, + ]} + /> + </View> + <View style={[a.flex_row, a.gap_md]}> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]} + /> + <View + style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_100}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_200}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_300}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_400}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_500}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_600}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_700}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_800}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_900}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_950}, + ]} + /> + <View + style={[ + a.flex_1, + {height: 60, backgroundColor: tokens.color.red_975}, + ]} + /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Shadows.tsx b/src/view/screens/Storybook/Shadows.tsx new file mode 100644 index 000000000..f92112395 --- /dev/null +++ b/src/view/screens/Storybook/Shadows.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {H1, Text} from '#/components/Typography' + +export function Shadows() { + const t = useTheme() + + return ( + <View style={[a.gap_md]}> + <H1>Shadows</H1> + + <View style={[a.flex_row, a.gap_5xl]}> + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_sm, + ]}> + <Text>shadow_sm</Text> + </View> + + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_md, + ]}> + <Text>shadow_md</Text> + </View> + + <View + style={[ + a.flex_1, + a.justify_center, + a.px_lg, + a.py_2xl, + t.atoms.bg, + t.atoms.shadow_lg, + ]}> + <Text>shadow_lg</Text> + </View> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Spacing.tsx b/src/view/screens/Storybook/Spacing.tsx new file mode 100644 index 000000000..d7faf93a8 --- /dev/null +++ b/src/view/screens/Storybook/Spacing.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text, H1} from '#/components/Typography' + +export function Spacing() { + const t = useTheme() + return ( + <View style={[a.gap_md]}> + <H1>Spacing</H1> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>2xs (2px)</Text> + <View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>xs (4px)</Text> + <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>sm (8px)</Text> + <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>md (12px)</Text> + <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>lg (16px)</Text> + <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>xl (20px)</Text> + <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>2xl (24px)</Text> + <View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>3xl (28px)</Text> + <View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>4xl (32px)</Text> + <View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} /> + </View> + + <View style={[a.flex_row, a.align_center]}> + <Text style={{width: 80}}>5xl (40px)</Text> + <View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} /> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Theming.tsx b/src/view/screens/Storybook/Theming.tsx new file mode 100644 index 000000000..a05443473 --- /dev/null +++ b/src/view/screens/Storybook/Theming.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import {Palette} from './Palette' + +export function Theming() { + const t = useTheme() + + return ( + <View style={[t.atoms.bg, a.gap_lg, a.p_xl]}> + <Palette /> + + <Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}> + theme.atoms.text_contrast_600 + </Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}> + theme.atoms.text_contrast_500 + </Text> + + <View style={[a.flex_1, t.atoms.border, a.border_t]} /> + <Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}> + theme.atoms.text_contrast_400 + </Text> + + <View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} /> + + <View style={[a.w_full, a.gap_md]}> + <View style={[t.atoms.bg, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg</Text> + </View> + <View style={[t.atoms.bg_contrast_25, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_25</Text> + </View> + <View style={[t.atoms.bg_contrast_50, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_50</Text> + </View> + <View style={[t.atoms.bg_contrast_100, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_100</Text> + </View> + <View style={[t.atoms.bg_contrast_200, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_200</Text> + </View> + <View style={[t.atoms.bg_contrast_300, a.justify_center, a.p_md]}> + <Text>theme.atoms.bg_contrast_300</Text> + </View> + </View> + </View> + ) +} diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx new file mode 100644 index 000000000..2e1f04a66 --- /dev/null +++ b/src/view/screens/Storybook/Typography.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a} from '#/alf' +import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography' + +export function Typography() { + return ( + <View style={[a.gap_md]}> + <H1>H1 Heading</H1> + <H2>H2 Heading</H2> + <H3>H3 Heading</H3> + <H4>H4 Heading</H4> + <H5>H5 Heading</H5> + <H6>H6 Heading</H6> + <P>P Paragraph</P> + + <Text style={[a.text_5xl]}>atoms.text_5xl</Text> + <Text style={[a.text_4xl]}>atoms.text_4xl</Text> + <Text style={[a.text_3xl]}>atoms.text_3xl</Text> + <Text style={[a.text_2xl]}>atoms.text_2xl</Text> + <Text style={[a.text_xl]}>atoms.text_xl</Text> + <Text style={[a.text_lg]}>atoms.text_lg</Text> + <Text style={[a.text_md]}>atoms.text_md</Text> + <Text style={[a.text_sm]}>atoms.text_sm</Text> + <Text style={[a.text_xs]}>atoms.text_xs</Text> + <Text style={[a.text_2xs]}>atoms.text_2xs</Text> + </View> + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx new file mode 100644 index 000000000..d8898f20e --- /dev/null +++ b/src/view/screens/Storybook/index.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import {View} from 'react-native' +import {CenteredView, ScrollView} from '#/view/com/util/Views' + +import {atoms as a, useTheme, ThemeProvider} from '#/alf' +import {useSetColorMode} from '#/state/shell' +import {Button} from '#/components/Button' + +import {Theming} from './Theming' +import {Typography} from './Typography' +import {Spacing} from './Spacing' +import {Buttons} from './Buttons' +import {Links} from './Links' +import {Forms} from './Forms' +import {Dialogs} from './Dialogs' +import {Breakpoints} from './Breakpoints' +import {Shadows} from './Shadows' +import {Icons} from './Icons' + +export function Storybook() { + const t = useTheme() + const setColorMode = useSetColorMode() + + return ( + <ScrollView> + <CenteredView style={[t.atoms.bg]}> + <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}> + <View style={[a.flex_row, a.align_start, a.gap_md]}> + <Button + variant="outline" + color="primary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('system')}> + System + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('light')}> + Light + </Button> + <Button + variant="solid" + color="secondary" + size="small" + label='Set theme to "system"' + onPress={() => setColorMode('dark')}> + Dark + </Button> + </View> + + <ThemeProvider theme="light"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dim"> + <Theming /> + </ThemeProvider> + <ThemeProvider theme="dark"> + <Theming /> + </ThemeProvider> + + <Typography /> + <Spacing /> + <Shadows /> + <Buttons /> + <Icons /> + <Links /> + <Forms /> + <Dialogs /> + <Breakpoints /> + </View> + </CenteredView> + </ScrollView> + ) +} diff --git a/src/view/screens/Support.tsx b/src/view/screens/Support.tsx index 6856f6759..9e7d36ec7 100644 --- a/src/view/screens/Support.tsx +++ b/src/view/screens/Support.tsx @@ -34,10 +34,10 @@ export const SupportScreen = (_props: Props) => { </Text> <Text style={[pal.text, s.p20]}> <Trans> - The support form has been moved. If you need help, please + The support form has been moved. If you need help, please{' '} <TextLink href={HELP_DESK_URL} - text=" click here" + text={_(msg`click here`)} style={pal.link} />{' '} or visit {HELP_DESK_URL} to get in touch with us. |