Merge branch 'main' into main

This commit is contained in:
Paul Frazee
2024-11-23 16:20:24 -08:00
committed by GitHub
340 changed files with 70407 additions and 40176 deletions
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {moderateProfile} from '@atproto/api'
-1
View File
@@ -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={{
+1 -1
View File
@@ -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')
}
}
+27 -16
View File
@@ -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>
)
}
+3 -5
View File
@@ -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 && (
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens} from '#/alf'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {
+1 -1
View File
@@ -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'
+9 -13
View File
@@ -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: {
+3 -4
View File
@@ -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])
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme} from '#/alf'
-2
View File
@@ -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'
+2 -2
View File
@@ -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) {
@@ -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 (
-1
View File
@@ -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'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import * as Progress from 'react-native-progress'
+28 -22
View File
@@ -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,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
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),
}),
)
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 (
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
+9 -24
View File
@@ -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)
)
}
@@ -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)
@@ -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>
)
}
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
@@ -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'
@@ -1,4 +1,3 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {
AppBskyActorDefs,
+3 -1
View File
@@ -1,3 +1,5 @@
export function SubtleWebHover({}: {hover: boolean}) {
import {ViewStyleProp} from '#/alf'
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
return null
}
+7 -6
View File
@@ -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,
]}
/>
)
+164 -150
View File
@@ -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 />
{preferences ? (
<>
<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 />
<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>
)
}
+10 -139
View File
@@ -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({
+12 -8
View File
@@ -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],
+1 -1
View File
@@ -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'
@@ -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 && {
+1 -1
View File
@@ -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'
+72 -29
View File
@@ -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>
@@ -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>
)
}
+10 -16
View File
@@ -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>
)
}
+9 -7
View File
@@ -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)
+3 -3
View File
@@ -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 (
+1 -1
View File
@@ -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={[
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+2 -1
View File
@@ -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'
+1 -1
View File
@@ -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(() => {
+41 -15
View File
@@ -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
+3 -3
View File
@@ -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 (
@@ -1,4 +1,3 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+20 -2
View File
@@ -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}
/>
</>
)
}
@@ -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"
/>
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -1,4 +1,3 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {useLingui} from '@lingui/react'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
+6
View File
@@ -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,
+2 -2
View File
@@ -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()
+5
View File
@@ -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',
})
+5
View File
@@ -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',
})
-1
View File
@@ -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'
+31 -9
View File
@@ -31,6 +31,37 @@ export function ContentHider({
ignoreMute?: boolean
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()
@@ -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} />
@@ -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>
)
-1
View File
@@ -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'
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, ViewStyle} from 'react-native'
import {ModerationCause, ModerationUI} from '@atproto/api'
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, ViewStyle} from 'react-native'
import {ModerationDecision} from '@atproto/api'
+10 -27
View File
@@ -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} />
</>
)
}