diff options
Diffstat (limited to 'src/components')
63 files changed, 542 insertions, 664 deletions
diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx index 5f790fb67..aea472512 100644 --- a/src/components/AvatarStack.tsx +++ b/src/components/AvatarStack.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {moderateProfile} from '@atproto/api' diff --git a/src/components/Button.tsx b/src/components/Button.tsx index aaac73bd3..3329dca05 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -458,7 +458,6 @@ export const Button = React.forwardRef<View, ButtonProps>( // @ts-ignore - this will always be a pressable ref={ref} aria-label={label} - aria-pressed={state.pressed} accessibilityLabel={label} disabled={disabled || false} accessibilityState={{ diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 0e78fcf97..c9455c5cc 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -75,7 +75,7 @@ export function Outer({ try { cb() } catch (e: any) { - logger.error('Error running close callback', e) + logger.error(e || 'Error running close callback') } } diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 41a39ffda..6b92eee3e 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -7,7 +7,6 @@ import { View, ViewStyle, } from 'react-native' -import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {DismissableLayer} from '@radix-ui/react-dismissable-layer' @@ -42,7 +41,6 @@ export function Outer({ onClose, }: React.PropsWithChildren<DialogOuterProps>) { const {_} = useLingui() - const t = useTheme() const {gtMobile} = useBreakpoints() const [isOpen, setIsOpen] = React.useState(false) const {setDialogIsOpen} = useDialogStateControlContext() @@ -118,16 +116,7 @@ export function Outer({ gtMobile ? a.p_lg : a.p_md, {overflowY: 'auto'}, ]}> - <Animated.View - entering={FadeIn.duration(150)} - // exiting={FadeOut.duration(150)} - style={[ - web(a.fixed), - a.inset_0, - {opacity: 0.8, backgroundColor: t.palette.black}, - ]} - /> - + <Backdrop /> <View style={[ a.w_full, @@ -164,7 +153,7 @@ export function Inner({ useFocusGuards() return ( <FocusScope loop asChild trapped> - <Animated.View + <View role="dialog" aria-role="dialog" aria-label={label} @@ -174,8 +163,6 @@ export function Inner({ onClick={stopPropagation} onStartShouldSetResponder={_ => true} onTouchEnd={stopPropagation} - entering={FadeInDown.duration(100)} - // exiting={FadeOut.duration(100)} style={flatten([ a.relative, a.rounded_md, @@ -188,6 +175,8 @@ export function Inner({ shadowColor: t.palette.black, shadowOpacity: t.name === 'light' ? 0.1 : 0.4, shadowRadius: 30, + // @ts-ignore web only + animation: 'fadeIn ease-out 0.1s', }, flatten(style), ])}> @@ -201,7 +190,7 @@ export function Inner({ {children} </View> </DismissableLayer> - </Animated.View> + </View> </FocusScope> ) } @@ -268,3 +257,25 @@ export function Close() { export function Handle() { return null } + +function Backdrop() { + const t = useTheme() + return ( + <View + style={{ + opacity: 0.8, + }}> + <View + style={[ + a.fixed, + a.inset_0, + { + backgroundColor: t.palette.black, + // @ts-ignore web only + animation: 'fadeIn ease-out 0.15s', + }, + ]} + /> + </View> + ) +} diff --git a/src/components/Dialog/shared.tsx b/src/components/Dialog/shared.tsx index 6f9bc2678..44a4f6b0b 100644 --- a/src/components/Dialog/shared.tsx +++ b/src/components/Dialog/shared.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleProp, TextStyle, View, ViewStyle} from 'react-native' -import {atoms as a, useTheme, web} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' export function Header({ @@ -29,10 +29,8 @@ export function Header({ a.border_b, t.atoms.border_contrast_medium, t.atoms.bg, - web([ - {borderRadiusTopLeft: a.rounded_md.borderRadius}, - {borderRadiusTopRight: a.rounded_md.borderRadius}, - ]), + {borderTopLeftRadius: a.rounded_md.borderRadius}, + {borderTopRightRadius: a.rounded_md.borderRadius}, style, ]}> {renderLeft && ( diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index ff0bbb045..e4891aacb 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf' diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 2819986b3..dc8e53b46 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/GradientFill.tsx b/src/components/GradientFill.tsx index 3c64c8960..fa39577d4 100644 --- a/src/components/GradientFill.tsx +++ b/src/components/GradientFill.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {LinearGradient} from 'expo-linear-gradient' import {atoms as a, tokens} from '#/alf' diff --git a/src/components/IconCircle.tsx b/src/components/IconCircle.tsx index 806d35c38..2119c9f8d 100644 --- a/src/components/IconCircle.tsx +++ b/src/components/IconCircle.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import { diff --git a/src/components/LikesDialog.tsx b/src/components/LikesDialog.tsx index 4c68596f7..cb000b433 100644 --- a/src/components/LikesDialog.tsx +++ b/src/components/LikesDialog.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo} from 'react' +import {useCallback, useMemo} from 'react' import {ActivityIndicator, FlatList, View} from 'react-native' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {msg, Trans} from '@lingui/macro' diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 054a543c1..ef31ea0c5 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -223,7 +223,7 @@ export function Link({ {...web({ hrefAttrs: { target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, + rel: isExternal ? 'noopener' : undefined, download, }, dataSet: { @@ -274,11 +274,6 @@ export function InlineLinkText({ onOut: onHoverOut, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const { - state: pressed, - onIn: onPressIn, - onOut: onPressOut, - } = useInteractionState() const flattenedStyle = flatten(style) || {} return ( @@ -289,19 +284,20 @@ export function InlineLinkText({ {...rest} style={[ {color: t.palette.primary_500}, - (hovered || focused || pressed) && + (hovered || focused) && !disableUnderline && { - ...web({outline: 0}), - textDecorationLine: 'underline', - textDecorationColor: flattenedStyle.color ?? t.palette.primary_500, + ...web({ + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: + flattenedStyle.color ?? t.palette.primary_500, + }), }, flattenedStyle, ]} role="link" onPress={download ? undefined : onPress} onLongPress={onLongPress} - onPressIn={onPressIn} - onPressOut={onPressOut} onFocus={onFocus} onBlur={onBlur} onMouseEnter={onHoverIn} @@ -311,7 +307,7 @@ export function InlineLinkText({ {...web({ hrefAttrs: { target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, + rel: isExternal ? 'noopener' : undefined, download, }, dataSet: { diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index e0b3be637..149554912 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -17,13 +17,12 @@ export function Loader(props: Props) { const rotation = useSharedValue(0) const animatedStyles = useAnimatedStyle(() => ({ - transform: [{rotate: rotation.value + 'deg'}], + transform: [{rotate: rotation.get() + 'deg'}], })) React.useEffect(() => { - rotation.value = withRepeat( - withTiming(360, {duration: 500, easing: Easing.linear}), - -1, + rotation.set(() => + withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1), ) }, [rotation]) diff --git a/src/components/Loader.web.tsx b/src/components/Loader.web.tsx index d8182673f..acf0acfc4 100644 --- a/src/components/Loader.web.tsx +++ b/src/components/Loader.web.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {atoms as a, flatten, useTheme} from '#/alf' diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index d68dcba51..37ad67e29 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react/prop-types */ - import React from 'react' import {Pressable, StyleProp, View, ViewStyle} from 'react-native' import {msg} from '@lingui/macro' diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 50b34ba99..668bd0f3c 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -283,8 +283,8 @@ export function DescriptionPlaceholder({ export type FollowButtonProps = { profile: AppBskyActorDefs.ProfileViewBasic moderationOpts: ModerationOpts - logContext: LogEvents['profile:follow:sampled']['logContext'] & - LogEvents['profile:unfollow:sampled']['logContext'] + logContext: LogEvents['profile:follow']['logContext'] & + LogEvents['profile:unfollow']['logContext'] } & Partial<ButtonProps> export function FollowButton(props: FollowButtonProps) { diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 4cda42fdb..3e58ced90 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -302,8 +302,8 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) { const animationStyle = { animation: currentState.stage === 'hiding' - ? `avatarHoverFadeOut ${HIDE_DURATION}ms both` - : `avatarHoverFadeIn ${SHOW_DURATION}ms both`, + ? `fadeOut ${HIDE_DURATION}ms both` + : `fadeIn ${SHOW_DURATION}ms both`, } return ( diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx index d0fd55d9c..299d1e69f 100644 --- a/src/components/ProgressGuide/List.tsx +++ b/src/components/ProgressGuide/List.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx index f2ceba52a..973ee1ac7 100644 --- a/src/components/ProgressGuide/Task.tsx +++ b/src/components/ProgressGuide/Task.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import * as Progress from 'react-native-progress' diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx index 69e008260..b26c718f8 100644 --- a/src/components/ProgressGuide/Toast.tsx +++ b/src/components/ProgressGuide/Toast.tsx @@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef< // animate the opacity then set isOpen to false when done const setIsntOpen = () => setIsOpen(false) - opacity.value = withTiming( - 0, - { - duration: 400, - easing: Easing.out(Easing.cubic), - }, - () => runOnJS(setIsntOpen)(), + opacity.set(() => + withTiming( + 0, + { + duration: 400, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(setIsntOpen)(), + ), ) }, [setIsOpen, opacity]) @@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef< // animate the vertical translation, the opacity, and the checkmark const playCheckmark = () => animatedCheckRef.current?.play() - opacity.value = 0 - opacity.value = withTiming( - 1, - { - duration: 100, + opacity.set(0) + opacity.set(() => + withTiming( + 1, + { + duration: 100, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(playCheckmark)(), + ), + ) + translateY.set(0) + translateY.set(() => + withTiming(insets.top + 10, { + duration: 500, easing: Easing.out(Easing.cubic), - }, - () => runOnJS(playCheckmark)(), + }), ) - translateY.value = 0 - translateY.value = withTiming(insets.top + 10, { - duration: 500, - easing: Easing.out(Easing.cubic), - }) // start the countdown timer to autoclose timeoutRef.current = setTimeout(close, visibleDuration || 5e3) @@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef< }, [winDim.width]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{translateY: translateY.value}], - opacity: opacity.value, + transform: [{translateY: translateY.get()}], + opacity: opacity.get(), })) return ( diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx index 039bbf123..df472241e 100644 --- a/src/components/ReportDialog/SelectLabelerView.tsx +++ b/src/components/ReportDialog/SelectLabelerView.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {AppBskyLabelerDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 8f6358dd5..6d7e50e48 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types' import {toShortUrl} from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' +import {isOnlyEmoji} from '#/alf/typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {InlineLinkText, LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' @@ -53,7 +54,6 @@ export function RichText({ const plainStyles = [a.leading_snug, flattenedStyle] const interactiveStyles = [ a.leading_snug, - a.pointer_events_auto, flatten(interactiveStyle), flattenedStyle, ] @@ -151,17 +151,14 @@ export function RichText({ />, ) } else { - els.push( - <Text key={key} emoji style={plainStyles}> - {segment.text} - </Text>, - ) + els.push(segment.text) } key++ } return ( <Text + emoji selectable={selectable} testID={testID} style={plainStyles} @@ -194,11 +191,6 @@ function RichTextTag({ onOut: onHoverOut, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const { - state: pressed, - onIn: onPressIn, - onOut: onPressOut, - } = useInteractionState() const navigation = useNavigation<NavigationProp>() const navigateToPage = React.useCallback(() => { @@ -228,8 +220,6 @@ function RichTextTag({ accessibilityRole: isNative ? 'button' : undefined, onPress: navigateToPage, onLongPress: openDialog, - onPressIn: onPressIn, - onPressOut: onPressOut, })} {...web({ onMouseEnter: onHoverIn, @@ -243,10 +233,12 @@ function RichTextTag({ cursor: 'pointer', }), {color: t.palette.primary_500}, - (hovered || focused || pressed) && { - ...web({outline: 0}), - textDecorationLine: 'underline', - textDecorationColor: t.palette.primary_500, + (hovered || focused) && { + ...web({ + outline: 0, + textDecorationLine: 'underline', + textDecorationColor: t.palette.primary_500, + }), }, style, ]}> @@ -256,10 +248,3 @@ function RichTextTag({ </React.Fragment> ) } - -export function isOnlyEmoji(text: string) { - return ( - text.length <= 15 && - /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text) - ) -} diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index ecd6225bb..c1c10c76b 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -40,7 +40,7 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>( ref, ) { const t = useTheme() - const bottomBarOffset = useBottomBarOffset(300) + const bottomBarOffset = useBottomBarOffset(headerHeight) const initialNumToRender = useInitialNumToRender() const {currentAccount} = useSession() const {data, refetch, isError} = useAllListMembersQuery(listUri) diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index 00afbdcfe..5f58a19df 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -14,6 +14,7 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' +import {useEmail} from '#/lib/hooks/useEmail' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {parseStarterPackUri} from '#/lib/strings/starter-pack' @@ -27,6 +28,7 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' +import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus' interface SectionRef { @@ -186,6 +188,9 @@ function Empty() { const followersDialogControl = useDialogControl() const errorDialogControl = useDialogControl() + const {needsEmailVerification} = useEmail() + const verifyEmailControl = useDialogControl() + const [isGenerating, setIsGenerating] = React.useState(false) const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ @@ -249,7 +254,13 @@ function Empty() { color="primary" size="small" disabled={isGenerating} - onPress={confirmDialogControl.open} + onPress={() => { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + confirmDialogControl.open() + } + }} style={{backgroundColor: 'transparent'}}> <ButtonText style={{color: 'white'}}> <Trans>Make one for me</Trans> @@ -262,7 +273,13 @@ function Empty() { color="primary" size="small" disabled={isGenerating} - onPress={() => navigation.navigate('StarterPackWizard')} + onPress={() => { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + navigation.navigate('StarterPackWizard') + } + }} style={{ backgroundColor: 'white', borderColor: 'white', @@ -318,6 +335,12 @@ function Empty() { onConfirm={generate} confirmButtonCta={_(msg`Retry`)} /> + <VerifyEmailDialog + reasonText={_( + msg`Before creating a starter pack, you must first verify your email.`, + )} + control={verifyEmailControl} + /> </LinearGradientBackground> ) } diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx index 997c6479c..354d7bc4e 100644 --- a/src/components/StarterPack/ShareDialog.tsx +++ b/src/components/StarterPack/ShareDialog.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {Image} from 'expo-image' import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx index 1e9f1c52d..b67a8d302 100644 --- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx +++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import {useRef} from 'react' import type {ListRenderItemInfo} from 'react-native' import {View} from 'react-native' import {AppBskyActorDefs, ModerationOpts} from '@atproto/api' diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index 44f01a154..75d2bff60 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {Keyboard, View} from 'react-native' import { AppBskyActorDefs, diff --git a/src/components/SubtleWebHover.tsx b/src/components/SubtleWebHover.tsx index e6f427237..5cbbfc898 100644 --- a/src/components/SubtleWebHover.tsx +++ b/src/components/SubtleWebHover.tsx @@ -1,3 +1,5 @@ -export function SubtleWebHover({}: {hover: boolean}) { +import {ViewStyleProp} from '#/alf' + +export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) { return null } diff --git a/src/components/SubtleWebHover.web.tsx b/src/components/SubtleWebHover.web.tsx index e98251e0d..8943147e4 100644 --- a/src/components/SubtleWebHover.web.tsx +++ b/src/components/SubtleWebHover.web.tsx @@ -1,10 +1,12 @@ -import React from 'react' import {StyleSheet, View} from 'react-native' import {isTouchDevice} from '#/lib/browser' -import {useTheme} from '#/alf' +import {useTheme, ViewStyleProp} from '#/alf' -export function SubtleWebHover({hover}: {hover: boolean}) { +export function SubtleWebHover({ + style, + hover, +}: ViewStyleProp & {hover: boolean}) { const t = useTheme() if (isTouchDevice) { return null @@ -26,9 +28,8 @@ export function SubtleWebHover({hover}: {hover: boolean}) { style={[ t.atoms.bg_contrast_25, styles.container, - { - opacity: hover ? opacity : 0, - }, + {opacity: hover ? opacity : 0}, + style, ]} /> ) diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index ae9fcdae2..310ecc4c2 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -40,9 +40,37 @@ export function TagMenu({ tag: string authorHandle?: string }>) { + const navigation = useNavigation<NavigationProp>() + return ( + <> + {children} + <Dialog.Outer control={control}> + <Dialog.Handle /> + <TagMenuInner + control={control} + tag={tag} + authorHandle={authorHandle} + navigation={navigation} + /> + </Dialog.Outer> + </> + ) +} + +function TagMenuInner({ + control, + tag, + authorHandle, + navigation, +}: { + control: Dialog.DialogOuterProps['control'] + tag: string + authorHandle?: string + // Passed down because on native, we don't use real portals (and context would be wrong). + navigation: NavigationProp +}) { const {_} = useLingui() const t = useTheme() - const navigation = useNavigation<NavigationProp>() const {isLoading: isPreferencesLoading, data: preferences} = usePreferencesQuery() const { @@ -79,32 +107,75 @@ export function TagMenu({ }, [tag, preferences?.moderationPrefs?.mutedWords]) return ( - <> - {children} - - <Dialog.Outer control={control}> - <Dialog.Handle /> - <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> - {isPreferencesLoading ? ( - <View style={[a.w_full, a.align_center]}> - <Loader size="lg" /> - </View> - ) : ( - <> + <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> + {isPreferencesLoading ? ( + <View style={[a.w_full, a.align_center]}> + <Loader size="lg" /> + </View> + ) : ( + <> + <View + style={[ + a.rounded_md, + a.border, + a.mb_md, + t.atoms.border_contrast_low, + t.atoms.bg_contrast_25, + ]}> + <Link + label={_(msg`View all posts with tag ${displayTag}`)} + {...createStaticClick(() => { + control.close(() => { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + }) + }) + })}> <View style={[ - a.rounded_md, - a.border, - a.mb_md, - t.atoms.border_contrast_low, - t.atoms.bg_contrast_25, + a.w_full, + a.flex_row, + a.align_center, + a.justify_start, + a.gap_md, + a.px_lg, + a.py_md, ]}> + <Search size="lg" style={[t.atoms.text_contrast_medium]} /> + <Text + numberOfLines={1} + ellipsizeMode="middle" + style={[ + a.flex_1, + a.text_md, + a.font_bold, + native({top: 2}), + t.atoms.text_contrast_medium, + ]}> + <Trans> + See{' '} + <Text style={[a.text_md, a.font_bold, t.atoms.text]}> + {displayTag} + </Text>{' '} + posts + </Trans> + </Text> + </View> + </Link> + + {authorHandle && !isInvalidHandle(authorHandle) && ( + <> + <Divider /> + <Link - label={_(msg`View all posts with tag ${displayTag}`)} + label={_( + msg`View all posts by @${authorHandle} with tag ${displayTag}`, + )} {...createStaticClick(() => { control.close(() => { navigation.push('Hashtag', { tag: encodeURIComponent(tag), + author: authorHandle, }) }) })}> @@ -118,7 +189,7 @@ export function TagMenu({ a.px_lg, a.py_md, ]}> - <Search size="lg" style={[t.atoms.text_contrast_medium]} /> + <Person size="lg" style={[t.atoms.text_contrast_medium]} /> <Text numberOfLines={1} ellipsizeMode="middle" @@ -134,143 +205,86 @@ export function TagMenu({ <Text style={[a.text_md, a.font_bold, t.atoms.text]}> {displayTag} </Text>{' '} - posts + posts by this user </Trans> </Text> </View> </Link> + </> + )} - {authorHandle && !isInvalidHandle(authorHandle) && ( - <> - <Divider /> - - <Link - label={_( - msg`View all posts by @${authorHandle} with tag ${displayTag}`, - )} - {...createStaticClick(() => { - control.close(() => { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - author: authorHandle, - }) - }) - })}> - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_start, - a.gap_md, - a.px_lg, - a.py_md, - ]}> - <Person - size="lg" - style={[t.atoms.text_contrast_medium]} - /> - <Text - numberOfLines={1} - ellipsizeMode="middle" - style={[ - a.flex_1, - a.text_md, - a.font_bold, - native({top: 2}), - t.atoms.text_contrast_medium, - ]}> - <Trans> - See{' '} - <Text - style={[a.text_md, a.font_bold, t.atoms.text]}> - {displayTag} - </Text>{' '} - posts by this user - </Trans> - </Text> - </View> - </Link> - </> - )} - - {preferences ? ( - <> - <Divider /> + {preferences ? ( + <> + <Divider /> - <Button - label={ - isMuted - ? _(msg`Unmute all ${displayTag} posts`) - : _(msg`Mute all ${displayTag} posts`) + <Button + label={ + isMuted + ? _(msg`Unmute all ${displayTag} posts`) + : _(msg`Mute all ${displayTag} posts`) + } + onPress={() => { + control.close(() => { + if (isMuted) { + resetUpsert() + removeMutedWords(removeableMuteWords) + } else { + resetRemove() + upsertMutedWord([ + { + value: tag, + targets: ['tag'], + actorTarget: 'all', + }, + ]) } - onPress={() => { - control.close(() => { - if (isMuted) { - resetUpsert() - removeMutedWords(removeableMuteWords) - } else { - resetRemove() - upsertMutedWord([ - { - value: tag, - targets: ['tag'], - actorTarget: 'all', - }, - ]) - } - }) - }}> - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_start, - a.gap_md, - a.px_lg, - a.py_md, - ]}> - <Mute - size="lg" - style={[t.atoms.text_contrast_medium]} - /> - <Text - numberOfLines={1} - ellipsizeMode="middle" - style={[ - a.flex_1, - a.text_md, - a.font_bold, - native({top: 2}), - t.atoms.text_contrast_medium, - ]}> - {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '} - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {displayTag} - </Text>{' '} - <Trans>posts</Trans> - </Text> - </View> - </Button> - </> - ) : null} - </View> + }) + }}> + <View + style={[ + a.w_full, + a.flex_row, + a.align_center, + a.justify_start, + a.gap_md, + a.px_lg, + a.py_md, + ]}> + <Mute size="lg" style={[t.atoms.text_contrast_medium]} /> + <Text + numberOfLines={1} + ellipsizeMode="middle" + style={[ + a.flex_1, + a.text_md, + a.font_bold, + native({top: 2}), + t.atoms.text_contrast_medium, + ]}> + {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '} + <Text style={[a.text_md, a.font_bold, t.atoms.text]}> + {displayTag} + </Text>{' '} + <Trans>posts</Trans> + </Text> + </View> + </Button> + </> + ) : null} + </View> - <Button - label={_(msg`Close this dialog`)} - size="small" - variant="ghost" - color="secondary" - onPress={() => control.close()}> - <ButtonText> - <Trans>Cancel</Trans> - </ButtonText> - </Button> - </> - )} - </Dialog.Inner> - </Dialog.Outer> - </> + <Button + label={_(msg`Close this dialog`)} + size="small" + variant="ghost" + color="secondary" + onPress={() => control.close()}> + <ButtonText> + <Trans>Cancel</Trans> + </ButtonText> + </Button> + </> + )} + </Dialog.Inner> ) } diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 69e073271..3e202cb8f 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -1,140 +1,15 @@ -import React from 'react' -import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native' import {UITextView} from 'react-native-uitextview' -import createEmojiRegex from 'emoji-regex' import {logger} from '#/logger' -import {isIOS, isNative} from '#/platform/detection' -import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf' +import {atoms, flatten, useAlf, useTheme, web} from '#/alf' +import { + childHasEmoji, + normalizeTextStyles, + renderChildrenWithEmoji, + TextProps, +} from '#/alf/typography' import {IS_DEV} from '#/env' - -export type StringChild = string | (string | null)[] - -export type TextProps = Omit<RNTextProps, 'children'> & { - /** - * Lets the user select text, to use the native copy and paste functionality. - */ - selectable?: boolean - /** - * Provides `data-*` attributes to the underlying `UITextView` component on - * web only. - */ - dataSet?: Record<string, string | number | undefined> - /** - * Appears as a small tooltip on web hover. - */ - title?: string -} & ( - | { - emoji?: true - children: StringChild - } - | { - emoji?: false - children: RNTextProps['children'] - } - ) - -const EMOJI = createEmojiRegex() - -export function childHasEmoji(children: React.ReactNode) { - return (Array.isArray(children) ? children : [children]).some( - child => typeof child === 'string' && createEmojiRegex().test(child), - ) -} - -export function childIsString( - children: React.ReactNode, -): children is StringChild { - return ( - typeof children === 'string' || - (Array.isArray(children) && - children.every(child => typeof child === 'string' || child === null)) - ) -} - -export function renderChildrenWithEmoji( - children: StringChild, - props: Omit<TextProps, 'children'> = {}, -) { - const normalized = Array.isArray(children) ? children : [children] - - return ( - <UITextView {...props}> - {normalized.map(child => { - if (typeof child !== 'string') return child - - const emojis = child.match(EMOJI) - - if (emojis === null) { - return child - } - - return child.split(EMOJI).map((stringPart, index) => ( - <UITextView key={index} {...props}> - {stringPart} - {emojis[index] ? ( - <UITextView - {...props} - style={[props?.style, {color: 'black', fontFamily: 'System'}]}> - {emojis[index]} - </UITextView> - ) : null} - </UITextView> - )) - })} - </UITextView> - ) -} - -/** - * Util to calculate lineHeight from a text size atom and a leading atom - * - * Example: - * `leading(atoms.text_md, atoms.leading_normal)` // => 24 - */ -export function leading< - Size extends {fontSize?: number}, - Leading extends {lineHeight?: number}, ->(textSize: Size, leading: Leading) { - const size = textSize?.fontSize || atoms.text_md.fontSize - const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight - return Math.round(size * lineHeight) -} - -/** - * Ensures that `lineHeight` defaults to a relative value of `1`, or applies - * other relative leading atoms. - * - * If the `lineHeight` value is > 2, we assume it's an absolute value and - * returns it as-is. - */ -export function normalizeTextStyles( - styles: StyleProp<TextStyle>, - { - fontScale, - fontFamily, - }: { - fontScale: number - fontFamily: Alf['fonts']['family'] - } & Pick<Alf, 'flags'>, -) { - const s = flatten(styles) - // should always be defined on these components - s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale - - if (s?.lineHeight) { - if (s.lineHeight !== 0 && s.lineHeight <= 2) { - s.lineHeight = Math.round(s.fontSize * s.lineHeight) - } - } else if (!isNative) { - s.lineHeight = s.fontSize - } - - applyFonts(s, fontFamily) - - return s -} +export type {TextProps} /** * Our main text component. Use this most of the time. @@ -162,10 +37,6 @@ export function Text({ `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`, ) } - - if (emoji && !childIsString(children)) { - logger.error('Text: when <Text emoji />, children can only be strings.') - } } const shared = { @@ -178,12 +49,12 @@ export function Text({ return ( <UITextView {...shared}> - {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children} + {renderChildrenWithEmoji(children, shared, emoji ?? false)} </UITextView> ) } -export function createHeadingElement({level}: {level: number}) { +function createHeadingElement({level}: {level: number}) { return function HeadingElement({style, ...rest}: TextProps) { const attr = web({ diff --git a/src/components/anim/AnimatedCheck.tsx b/src/components/anim/AnimatedCheck.tsx index 7fdfc14cf..60407274e 100644 --- a/src/components/anim/AnimatedCheck.tsx +++ b/src/components/anim/AnimatedCheck.tsx @@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef< const checkAnim = useSharedValue(0) const circleAnimatedProps = useAnimatedProps(() => ({ - strokeDashoffset: 166 - circleAnim.value * 166, + strokeDashoffset: 166 - circleAnim.get() * 166, })) const checkAnimatedProps = useAnimatedProps(() => ({ - strokeDashoffset: 48 - 48 * checkAnim.value, + strokeDashoffset: 48 - 48 * checkAnim.get(), })) const play = React.useCallback( (cb?: () => void) => { - circleAnim.value = 0 - checkAnim.value = 0 + circleAnim.set(0) + checkAnim.set(0) - circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear}) - checkAnim.value = withDelay( - 500, - withTiming(1, {duration: 300, easing: Easing.linear}, cb), + circleAnim.set(() => + withTiming(1, {duration: 500, easing: Easing.linear}), + ) + checkAnim.set(() => + withDelay( + 500, + withTiming(1, {duration: 300, easing: Easing.linear}, cb), + ), ) }, [circleAnim, checkAnim], diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx index 824155d8b..086d43f95 100644 --- a/src/components/dialogs/EmbedConsent.tsx +++ b/src/components/dialogs/EmbedConsent.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 0b8b386d3..8536001da 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -256,6 +256,9 @@ export function PostInteractionSettingsForm({ } else { newSelected.splice(i, 1) } + if (newSelected.length === 0) { + newSelected.push({type: 'everybody'}) + } onChangeThreadgateAllowUISettings(newSelected) } @@ -306,7 +309,7 @@ export function PostInteractionSettingsForm({ } value={quotesEnabled} onChange={onChangeQuotesEnabled} - style={[, a.justify_between, a.pt_xs]}> + style={[a.justify_between, a.pt_xs]}> <Text style={[t.atoms.text_contrast_medium]}> {quotesEnabled ? ( <Trans>Quote posts enabled</Trans> @@ -483,7 +486,7 @@ function Selectable({ a.justify_between, a.rounded_sm, a.p_md, - {height: 40}, // for consistency with checkmark icon visible or not + {minHeight: 40}, // for consistency with checkmark icon visible or not t.atoms.bg_contrast_50, (hovered || focused) && t.atoms.bg_contrast_100, isSelected && { diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx index daad01d2a..9acefa8fc 100644 --- a/src/components/dialogs/SwitchAccount.tsx +++ b/src/components/dialogs/SwitchAccount.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import {useCallback} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx index 8dfb9bc49..ced9171ce 100644 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ b/src/components/dialogs/VerifyEmailDialog.tsx @@ -18,8 +18,14 @@ import {Text} from '#/components/Typography' export function VerifyEmailDialog({ control, + onCloseWithoutVerifying, + onCloseAfterVerifying, + reasonText, }: { control: Dialog.DialogControlProps + onCloseWithoutVerifying?: () => void + onCloseAfterVerifying?: () => void + reasonText?: string }) { const agent = useAgent() @@ -30,18 +36,24 @@ export function VerifyEmailDialog({ control={control} onClose={async () => { if (!didVerify) { + onCloseWithoutVerifying?.() return } try { await agent.resumeSession(agent.session!) + onCloseAfterVerifying?.() } catch (e: unknown) { logger.error(String(e)) return } }}> <Dialog.Handle /> - <Inner control={control} setDidVerify={setDidVerify} /> + <Inner + control={control} + setDidVerify={setDidVerify} + reasonText={reasonText} + /> </Dialog.Outer> ) } @@ -49,9 +61,11 @@ export function VerifyEmailDialog({ export function Inner({ control, setDidVerify, + reasonText, }: { control: Dialog.DialogControlProps setDidVerify: (value: boolean) => void + reasonText?: string }) { const {_} = useLingui() const {currentAccount} = useSession() @@ -132,34 +146,63 @@ export function Inner({ <ErrorMessage message={error} /> </View> ) : null} - <Text style={[a.text_md, a.leading_snug]}> - {currentStep === 'StepOne' ? ( - <> - <Trans> - You'll receive an email at{' '} - <Text style={[a.text_md, a.leading_snug, a.font_bold]}> - {currentAccount?.email} - </Text>{' '} - to verify it's you. - </Trans>{' '} - <InlineLinkText - to="#" - label={_(msg`Change email address`)} - style={[a.text_md, a.leading_snug]} - onPress={e => { - e.preventDefault() - control.close(() => { - openModal({name: 'change-email'}) - }) - return false - }}> - <Trans>Need to change it?</Trans> - </InlineLinkText> - </> - ) : ( - uiStrings[currentStep].message - )} - </Text> + {currentStep === 'StepOne' ? ( + <View> + {reasonText ? ( + <View style={[a.gap_sm]}> + <Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text> + <Text style={[a.text_md, a.leading_snug]}> + Don't have access to{' '} + <Text style={[a.text_md, a.leading_snug, a.font_bold]}> + {currentAccount?.email} + </Text> + ?{' '} + <InlineLinkText + to="#" + label={_(msg`Change email address`)} + style={[a.text_md, a.leading_snug]} + onPress={e => { + e.preventDefault() + control.close(() => { + openModal({name: 'change-email'}) + }) + return false + }}> + <Trans>Change your email address</Trans> + </InlineLinkText> + . + </Text> + </View> + ) : ( + <Text style={[a.text_md, a.leading_snug]}> + <Trans> + You'll receive an email at{' '} + <Text style={[a.text_md, a.leading_snug, a.font_bold]}> + {currentAccount?.email} + </Text>{' '} + to verify it's you. + </Trans>{' '} + <InlineLinkText + to="#" + label={_(msg`Change email address`)} + style={[a.text_md, a.leading_snug]} + onPress={e => { + e.preventDefault() + control.close(() => { + openModal({name: 'change-email'}) + }) + return false + }}> + <Trans>Need to change it?</Trans> + </InlineLinkText> + </Text> + )} + </View> + ) : ( + <Text style={[a.text_md, a.leading_snug]}> + {uiStrings[currentStep].message} + </Text> + )} </View> {currentStep === 'StepTwo' ? ( <View> diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx deleted file mode 100644 index f29dc356d..000000000 --- a/src/components/dialogs/nuxs/NeueTypography.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings' -import {atoms as a, useAlf, useTheme} from '#/alf' -import * as Dialog from '#/components/Dialog' -import {useNuxDialogContext} from '#/components/dialogs/nuxs' -import {Divider} from '#/components/Divider' -import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' -import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' -import {Text} from '#/components/Typography' - -export function NeueTypography() { - const t = useTheme() - const {_} = useLingui() - const nuxDialogs = useNuxDialogContext() - const control = Dialog.useDialogControl() - const {fonts} = useAlf() - - Dialog.useAutoOpen(control, 3e3) - - const onClose = React.useCallback(() => { - nuxDialogs.dismissActiveNux() - }, [nuxDialogs]) - - const onChangeFontFamily = React.useCallback( - (values: string[]) => { - const next = values[0] === 'system' ? 'system' : 'theme' - fonts.setFontFamily(next) - }, - [fonts], - ) - - const onChangeFontScale = React.useCallback( - (values: string[]) => { - const next = values[0] || ('0' as any) - fonts.setFontScale(next) - }, - [fonts], - ) - - return ( - <Dialog.Outer control={control} onClose={onClose}> - <Dialog.Handle /> - <Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}> - <View style={[a.gap_xl]}> - <View style={[a.gap_md]}> - <Text style={[a.text_3xl, a.font_heavy]}> - <Trans>New font settings ✨</Trans> - </Text> - <Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}> - <Trans> - We're introducing a new theme font, along with adjustable font - sizing. - </Trans> - </Text> - <Text - style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> - <Trans> - You can adjust these in your Appearance Settings later. - </Trans> - </Text> - </View> - - <Divider /> - - <View style={[a.gap_lg]}> - <AppearanceToggleButtonGroup - title={_(msg`Font`)} - description={_( - msg`For the best experience, we recommend using the theme font.`, - )} - icon={Aa} - items={[ - { - label: _(msg`System`), - name: 'system', - }, - { - label: _(msg`Theme`), - name: 'theme', - }, - ]} - values={[fonts.family]} - onChange={onChangeFontFamily} - /> - - <AppearanceToggleButtonGroup - title={_(msg`Font size`)} - icon={TextSize} - items={[ - { - label: _(msg`Smaller`), - name: '-1', - }, - { - label: _(msg`Default`), - name: '0', - }, - { - label: _(msg`Larger`), - name: '1', - }, - ]} - values={[fonts.scale]} - onChange={onChangeFontScale} - /> - </View> - </View> - - <Dialog.Close /> - </Dialog.ScrollableInner> - </Dialog.Outer> - ) -} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index d17615aeb..701ae84e6 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -19,7 +19,6 @@ import {useOnboardingState} from '#/state/shell' /* * NUXs */ -import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography' import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' import {IS_DEV} from '#/env' @@ -36,19 +35,7 @@ const queuedNuxs: { currentProfile: AppBskyActorDefs.ProfileViewDetailed preferences: UsePreferencesQueryResponse }) => boolean -}[] = [ - { - id: Nux.NeueTypography, - enabled(props) { - if (props.currentProfile.createdAt) { - if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) { - return true - } - } - return false - }, - }, -] +}[] = [] const Context = React.createContext<Context>({ activeNux: undefined, @@ -66,7 +53,14 @@ export function NuxDialogs() { const onboardingActive = useOnboardingState().isActive const isLoading = - !currentAccount || !preferences || !profile || onboardingActive + onboardingActive || + !currentAccount || + !preferences || + !profile || + // Profile isn't legit ready until createdAt is a real date. + !profile.createdAt || + profile.createdAt === '0001-01-01T00:00:00.000Z' // TODO: Fix this in AppView. + return !isLoading ? ( <Inner currentAccount={currentAccount} @@ -174,7 +168,7 @@ function Inner({ return ( <Context.Provider value={ctx}> - {activeNux === Nux.NeueTypography && <NeueTypography />} + {/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/} </Context.Provider> ) } diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index b77516e7b..a087fed3f 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -34,7 +34,7 @@ export function ActionsWrapper({ const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) const open = React.useCallback(() => { @@ -46,7 +46,7 @@ export function ActionsWrapper({ const shrink = React.useCallback(() => { 'worklet' cancelAnimation(scale) - scale.value = withTiming(1, {duration: 200}) + scale.set(() => withTiming(1, {duration: 200})) }, [scale]) const doubleTapGesture = Gesture.Tap() @@ -58,11 +58,13 @@ export function ActionsWrapper({ const pressAndHoldGesture = Gesture.LongPress() .onStart(() => { 'worklet' - scale.value = withTiming(1.05, {duration: 200}, finished => { - if (!finished) return - runOnJS(open)() - shrink() - }) + scale.set(() => + withTiming(1.05, {duration: 200}, finished => { + if (!finished) return + runOnJS(open)() + shrink() + }), + ) }) .onTouchesUp(shrink) .onTouchesMove(shrink) diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx index ffd022f56..042c3ad76 100644 --- a/src/components/dms/ChatEmptyPill.tsx +++ b/src/components/dms/ChatEmptyPill.tsx @@ -42,12 +42,12 @@ export function ChatEmptyPill() { const onPressIn = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1.075, {duration: 100}) + scale.set(() => withTiming(1.075, {duration: 100})) }, [scale]) const onPressOut = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1, {duration: 100}) + scale.set(() => withTiming(1, {duration: 100})) }, [scale]) const onPress = React.useCallback(() => { @@ -61,7 +61,7 @@ export function ChatEmptyPill() { }, [playHaptic, prompts.length]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) return ( diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index affc292c1..e1f8df10b 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -115,7 +115,7 @@ let ConvoMenu = ({ {...props} onPress={() => { Keyboard.dismiss() - // eslint-disable-next-line react/prop-types -- eslint is confused by the name `props` + props.onPress() }} style={[ diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx index 2baa07b46..cc18c1ab4 100644 --- a/src/components/dms/LeaveConvoPrompt.tsx +++ b/src/components/dms/LeaveConvoPrompt.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 52220e2ca..79f0997fd 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types' import {useSession} from '#/state/session' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {atoms as a, useTheme} from '#/alf' +import {isOnlyEmoji} from '#/alf/typography' import {ActionsWrapper} from '#/components/dms/ActionsWrapper' import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' -import {isOnlyEmoji, RichText} from '../RichText' +import {RichText} from '../RichText' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' import {localDateString} from './util' diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx index c1867e727..90ee5b979 100644 --- a/src/components/dms/MessageMenu.tsx +++ b/src/components/dms/MessageMenu.tsx @@ -62,7 +62,7 @@ export let MessageMenu = ({ message.text, langPrefs.primaryLanguage, ) - openLink(translatorUrl) + openLink(translatorUrl, true) }, [langPrefs.primaryLanguage, message.text, openLink]) const onDelete = React.useCallback(() => { diff --git a/src/components/dms/MessageProfileButton.tsx b/src/components/dms/MessageProfileButton.tsx index 932982d05..22936b4c0 100644 --- a/src/components/dms/MessageProfileButton.tsx +++ b/src/components/dms/MessageProfileButton.tsx @@ -3,14 +3,18 @@ import {View} from 'react-native' import {AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' +import {useEmail} from '#/lib/hooks/useEmail' +import {NavigationProp} from '#/lib/routes/types' import {logEvent} from '#/lib/statsig/statsig' import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members' import {atoms as a, useTheme} from '#/alf' -import {ButtonIcon} from '#/components/Button' +import {Button, ButtonIcon} from '#/components/Button' import {canBeMessaged} from '#/components/dms/util' import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message' -import {Link} from '#/components/Link' +import {useDialogControl} from '../Dialog' +import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog' export function MessageProfileButton({ profile, @@ -19,15 +23,29 @@ export function MessageProfileButton({ }) { const {_} = useLingui() const t = useTheme() + const navigation = useNavigation<NavigationProp>() + const {needsEmailVerification} = useEmail() + const verifyEmailControl = useDialogControl() const {data: convo, isPending} = useMaybeConvoForUser(profile.did) const onPress = React.useCallback(() => { + if (!convo?.id) { + return + } + + if (needsEmailVerification) { + verifyEmailControl.open() + return + } + if (convo && !convo.lastMessage) { logEvent('chat:create', {logContext: 'ProfileHeader'}) } logEvent('chat:open', {logContext: 'ProfileHeader'}) - }, [convo]) + + navigation.navigate('MessagesConversation', {conversation: convo.id}) + }, [needsEmailVerification, verifyEmailControl, convo, navigation]) if (isPending) { // show pending state based on declaration @@ -53,18 +71,26 @@ export function MessageProfileButton({ if (convo) { return ( - <Link - testID="dmBtn" - size="small" - color="secondary" - variant="solid" - shape="round" - label={_(msg`Message ${profile.handle}`)} - to={`/messages/${convo.id}`} - style={[a.justify_center]} - onPress={onPress}> - <ButtonIcon icon={Message} size="md" /> - </Link> + <> + <Button + accessibilityRole="button" + testID="dmBtn" + size="small" + color="secondary" + variant="solid" + shape="round" + label={_(msg`Message ${profile.handle}`)} + style={[a.justify_center]} + onPress={onPress}> + <ButtonIcon icon={Message} size="md" /> + </Button> + <VerifyEmailDialog + reasonText={_( + msg`Before you may message another user, you must first verify your email.`, + )} + control={verifyEmailControl} + /> + </> ) } else { return null diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx index 2f7ff8f4b..e3bc0c1f8 100644 --- a/src/components/dms/NewMessagesPill.tsx +++ b/src/components/dms/NewMessagesPill.tsx @@ -35,12 +35,12 @@ export function NewMessagesPill({ const onPressIn = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1.075, {duration: 100}) + scale.set(() => withTiming(1.075, {duration: 100})) }, [scale]) const onPressOut = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1, {duration: 100}) + scale.set(() => withTiming(1, {duration: 100})) }, [scale]) const onPress = React.useCallback(() => { @@ -49,7 +49,7 @@ export function NewMessagesPill({ }, [onPressInner, playHaptic]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) return ( diff --git a/src/components/dms/ReportConversationPrompt.tsx b/src/components/dms/ReportConversationPrompt.tsx index 610cfbcf9..6bb26a60f 100644 --- a/src/components/dms/ReportConversationPrompt.tsx +++ b/src/components/dms/ReportConversationPrompt.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index e80fef2d7..c7fedb488 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -1,7 +1,8 @@ -import React, {useCallback} from 'react' +import {useCallback} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useEmail} from '#/lib/hooks/useEmail' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' @@ -9,6 +10,8 @@ import {FAB} from '#/view/com/util/fab/FAB' import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' +import {useDialogControl} from '#/components/Dialog' +import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SearchablePeopleList} from './SearchablePeopleList' @@ -21,6 +24,8 @@ export function NewChat({ }) { const t = useTheme() const {_} = useLingui() + const {needsEmailVerification} = useEmail() + const verifyEmailControl = useDialogControl() const {mutate: createChat} = useGetConvoForMembers({ onSuccess: data => { @@ -48,7 +53,13 @@ export function NewChat({ <> <FAB testID="newChatFAB" - onPress={control.open} + onPress={() => { + if (needsEmailVerification) { + verifyEmailControl.open() + } else { + control.open() + } + }} icon={<Plus size="lg" fill={t.palette.white} />} accessibilityRole="button" accessibilityLabel={_(msg`New chat`)} @@ -62,6 +73,13 @@ export function NewChat({ onSelectChat={onCreateChat} /> </Dialog.Outer> + + <VerifyEmailDialog + reasonText={_( + msg`Before you may message another user, you must first verify your email.`, + )} + control={verifyEmailControl} + /> </> ) } diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx index a5687a096..bc7fcbe56 100644 --- a/src/components/dms/dialogs/SearchablePeopleList.tsx +++ b/src/components/dms/dialogs/SearchablePeopleList.tsx @@ -278,7 +278,7 @@ export function SearchablePeopleList({ ) : null} </View> - <View style={[, web([a.pt_xs])]}> + <View style={web([a.pt_xs])}> <SearchInput inputRef={inputRef} value={searchText} @@ -313,6 +313,7 @@ export function SearchablePeopleList({ web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]), native({height: '100%'}), ]} + webInnerContentContainerStyle={a.py_0} webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]} keyboardDismissMode="on-drag" /> diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 38b558343..4bb27ae69 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import {useCallback} from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/forms/DateField/index.shared.tsx b/src/components/forms/DateField/index.shared.tsx index 814bbed7c..7438f5622 100644 --- a/src/components/forms/DateField/index.shared.tsx +++ b/src/components/forms/DateField/index.shared.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {Pressable, View} from 'react-native' import {useLingui} from '@lingui/react' diff --git a/src/components/forms/FormError.tsx b/src/components/forms/FormError.tsx index 8ab6e3f35..d51243d50 100644 --- a/src/components/forms/FormError.tsx +++ b/src/components/forms/FormError.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' diff --git a/src/components/hooks/dates.ts b/src/components/hooks/dates.ts index 89c483d3c..28bb7635c 100644 --- a/src/components/hooks/dates.ts +++ b/src/components/hooks/dates.ts @@ -16,6 +16,7 @@ import { es, fi, fr, + gl, hi, hu, id, @@ -23,11 +24,13 @@ import { ja, ko, nl, + pl, ptBR, ru, th, tr, uk, + vi, zhCN, zhHK, zhTW, @@ -48,6 +51,7 @@ const locales: Record<AppLanguage, Locale | undefined> = { fi, fr, ga: undefined, + gl, hi, hu, id, @@ -55,11 +59,13 @@ const locales: Record<AppLanguage, Locale | undefined> = { ja, ko, nl, + pl, ['pt-BR']: ptBR, ru, th, tr, uk, + vi, ['zh-CN']: zhCN, ['zh-HK']: zhHK, ['zh-TW']: zhTW, diff --git a/src/components/hooks/useFollowMethods.ts b/src/components/hooks/useFollowMethods.ts index 31a1e43da..d67c3690f 100644 --- a/src/components/hooks/useFollowMethods.ts +++ b/src/components/hooks/useFollowMethods.ts @@ -15,8 +15,8 @@ export function useFollowMethods({ logContext, }: { profile: Shadow<AppBskyActorDefs.ProfileViewBasic> - logContext: LogEvents['profile:follow:sampled']['logContext'] & - LogEvents['profile:unfollow:sampled']['logContext'] + logContext: LogEvents['profile:follow']['logContext'] & + LogEvents['profile:unfollow']['logContext'] }) { const {_} = useLingui() const requireAuth = useRequireAuth() diff --git a/src/components/icons/CalendarClock.tsx b/src/components/icons/CalendarClock.tsx new file mode 100644 index 000000000..52ba8094e --- /dev/null +++ b/src/components/icons/CalendarClock.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx new file mode 100644 index 000000000..ba0b417a9 --- /dev/null +++ b/src/components/icons/ChainLink.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z', +}) diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx index e83f96f0b..996ecb626 100644 --- a/src/components/icons/common.tsx +++ b/src/components/icons/common.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {StyleSheet, TextProps} from 'react-native' import type {PathProps, SvgProps} from 'react-native-svg' import {Defs, LinearGradient, Stop} from 'react-native-svg' diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index 67aef67b4..69193592a 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -32,6 +32,37 @@ export function ContentHider({ style?: StyleProp<ViewStyle> childContainerStyle?: StyleProp<ViewStyle> }>) { + const blur = modui?.blurs[0] + if (!blur || (ignoreMute && isJustAMute(modui))) { + return ( + <View testID={testID} style={style}> + {children} + </View> + ) + } + return ( + <ContentHiderActive + testID={testID} + modui={modui} + style={style} + childContainerStyle={childContainerStyle}> + {children} + </ContentHiderActive> + ) +} + +function ContentHiderActive({ + testID, + modui, + style, + childContainerStyle, + children, +}: React.PropsWithChildren<{ + testID?: string + modui: ModerationUI + style?: StyleProp<ViewStyle> + childContainerStyle?: StyleProp<ViewStyle> +}>) { const t = useTheme() const {_} = useLingui() const {gtMobile} = useBreakpoints() @@ -40,7 +71,6 @@ export function ContentHider({ const {labelDefs} = useLabelDefinitions() const globalLabelStrings = useGlobalLabelStrings() const {i18n} = useLingui() - const blur = modui?.blurs[0] const desc = useModerationCauseDescription(blur) @@ -99,14 +129,6 @@ export function ContentHider({ globalLabelStrings, ]) - if (!blur || (ignoreMute && isJustAMute(modui))) { - return ( - <View testID={testID} style={style}> - {children} - </View> - ) - } - return ( <View testID={testID} style={[a.overflow_hidden, style]}> <ModerationDetailsDialog control={control} modcause={blur} /> diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx index ecdbcfd25..e6f18f1d6 100644 --- a/src/components/moderation/LabelPreference.tsx +++ b/src/components/moderation/LabelPreference.tsx @@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) { <View style={[ a.flex_row, - a.gap_md, + a.gap_sm, a.px_lg, a.py_lg, a.justify_between, @@ -74,10 +74,9 @@ export function Buttons({ hideLabel?: string }) { const {_} = useLingui() - const {gtPhone} = useBreakpoints() return ( - <View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}> + <View style={[{minHeight: 35}, a.w_full]}> <ToggleButton.Group label={_( msg`Configure content filtering setting for category: ${name}`, @@ -259,7 +258,7 @@ export function LabelerLabelPreference({ </Content> {showConfig && ( - <View style={[gtPhone ? undefined : a.w_full]}> + <> {cantConfigure ? ( <View style={[ @@ -290,7 +289,7 @@ export function LabelerLabelPreference({ hideLabel={hideLabel} /> )} - </View> + </> )} </Outer> ) diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx index 33ede3ed2..681599807 100644 --- a/src/components/moderation/LabelsOnMe.tsx +++ b/src/components/moderation/LabelsOnMe.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api' import {msg, Plural} from '@lingui/macro' diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index ef40a7996..bdbb2daa5 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {ModerationCause} from '@atproto/api' import {msg, Trans} from '@lingui/macro' diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx index 6c4e5f8c8..a68a650d6 100644 --- a/src/components/moderation/PostAlerts.tsx +++ b/src/components/moderation/PostAlerts.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {StyleProp, ViewStyle} from 'react-native' import {ModerationCause, ModerationUI} from '@atproto/api' diff --git a/src/components/moderation/ProfileHeaderAlerts.tsx b/src/components/moderation/ProfileHeaderAlerts.tsx index 891caec18..4ac561fd9 100644 --- a/src/components/moderation/ProfileHeaderAlerts.tsx +++ b/src/components/moderation/ProfileHeaderAlerts.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {StyleProp, ViewStyle} from 'react-native' import {ModerationDecision} from '@atproto/api' diff --git a/src/components/video/PlayButtonIcon.tsx b/src/components/video/PlayButtonIcon.tsx index 8e0a6bb7a..801cad9b9 100644 --- a/src/components/video/PlayButtonIcon.tsx +++ b/src/components/video/PlayButtonIcon.tsx @@ -1,4 +1,3 @@ -import React from 'react' import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' @@ -10,39 +9,23 @@ export function PlayButtonIcon({size = 32}: {size?: number}) { const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25 return ( - <View - style={[ - a.rounded_full, - a.overflow_hidden, - a.align_center, - a.justify_center, - t.atoms.shadow_lg, - { - width: size + size / 1.5, - height: size + size / 1.5, - }, - ]}> + <> <View style={[ - a.absolute, - a.inset_0, + a.rounded_full, { backgroundColor: bg, + shadowColor: 'black', + shadowRadius: 32, + shadowOpacity: 0.5, + elevation: 24, + width: size + size / 1.5, + height: size + size / 1.5, opacity: 0.7, }, ]} /> - <PlayIcon - width={size} - fill={fg} - style={[ - a.relative, - a.z_10, - { - left: size / 50, - }, - ]} - /> - </View> + <PlayIcon width={size} fill={fg} style={a.absolute} /> + </> ) } |