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
+37 -34
View File
@@ -69,6 +69,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {AppProfiler} from './AppProfiler'
SplashScreen.preventAutoHideAsync()
@@ -184,40 +185,42 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
<AppProfiler>
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
</AppProfiler>
)
}
+29
View File
@@ -0,0 +1,29 @@
import React, {Profiler} from 'react'
// Don't let it get stripped out in profiling builds (which apply production Babel preset).
const log = (global as any)['con' + 'sole'].log
function onRender(id: string, phase: string, actualDuration: number) {
if (!__DEV__) {
// This block of code will exist in the production build of the app.
// However, only profiling builds of React call `onRender` so it's dead code in actual production.
const message = `<Profiler> ${id}:${phase} ${
actualDuration > 500
? '(╯°□°)╯ '
: actualDuration > 100
? '[!!] '
: actualDuration > 16
? '[!] '
: ''
}${Math.round(actualDuration)}ms`
log(message)
}
}
export function AppProfiler({children}: {children: React.ReactNode}) {
return (
<Profiler id="app" onRender={onRender}>
{children}
</Profiler>
)
}
+2 -8
View File
@@ -32,7 +32,7 @@ import {
import {RouteParams, State} from '#/lib/routes/types'
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
import {bskyTitle} from '#/lib/strings/headings'
import {isAndroid, isNative, isWeb} from '#/platform/detection'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useSession} from '#/state/session'
@@ -453,7 +453,6 @@ function HomeTabNavigator() {
return (
<HomeTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -472,7 +471,6 @@ function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -490,7 +488,6 @@ function NotificationsTabNavigator() {
return (
<NotificationsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -512,7 +509,6 @@ function MyProfileTabNavigator() {
return (
<MyProfileTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -538,7 +534,6 @@ function MessagesTabNavigator() {
return (
<MessagesTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -572,7 +567,6 @@ const FlatNavigator = () => {
<Flat.Navigator
screenListeners={screenListeners}
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -696,7 +690,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
onStateChange={() => {
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications:sampled', {})
logEvent('router:navigate:notifications', {})
}
}}
onReady={() => {
+40 -32
View File
@@ -81,40 +81,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
{
scale: interpolate(
outroLogo.value,
outroLogo.get(),
[0, 0.08, 1],
[1, 0.8, 500],
'clamp',
),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const bottomLogoAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const reducedLogoAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const logoWrapperAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[1, 1, 0, 0],
'clamp',
@@ -126,11 +126,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
},
],
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[0, 0, 1, 1],
'clamp',
@@ -146,29 +146,37 @@ export function Splash(props: React.PropsWithChildren<Props>) {
if (isReady) {
SplashScreen.hideAsync()
.then(() => {
intro.value = withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.value = 0.1
// outroApp.value = 0.1
outroLogo.value = withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
})
outroAppOpacity.value = withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
})
},
intro.set(() =>
withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.set(0.1)
// outroApp.set(0.1)
outroLogo.set(() =>
withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
),
)
outroApp.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
}),
)
outroAppOpacity.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
}),
)
},
),
)
})
.catch(() => {})
+126
View File
@@ -0,0 +1,126 @@
import React, {Children} from 'react'
import {TextProps as RNTextProps} from 'react-native'
import {StyleProp, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {isNative} from '#/platform/detection'
import {isIOS} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten} from '#/alf'
/**
* 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 StringChild = string | (string | null)[]
export type TextProps = RNTextProps & {
/**
* 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
/**
* Whether the children could possibly contain emoji.
*/
emoji?: boolean
}
const EMOJI = createEmojiRegex()
export function childHasEmoji(children: React.ReactNode) {
let hasEmoji = false
Children.forEach(children, child => {
if (typeof child === 'string' && createEmojiRegex().test(child)) {
hasEmoji = true
}
})
return hasEmoji
}
export function renderChildrenWithEmoji(
children: React.ReactNode,
props: Omit<TextProps, 'children'> = {},
emoji: boolean,
) {
if (!isIOS || !emoji) {
return children
}
return Children.map(children, child => {
if (typeof child !== 'string') return child
const emojis = child.match(EMOJI)
if (emojis === null) {
return child
}
return child.split(EMOJI).map((stringPart, index) => [
stringPart,
emojis[index] ? (
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null,
])
})
}
const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
export function isOnlyEmoji(text: string) {
return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
}
-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} />
</>
)
}
+2 -1
View File
@@ -21,7 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
// code and update this number with each release until we can get the
// server route done.
// -prf
export const JOINED_THIS_WEEK = 650000 // estimate as of 10/28/24
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
@@ -154,6 +154,7 @@ export const SUPPORTED_MIME_TYPES = [
'video/mpeg',
'video/webm',
'video/quicktime',
'image/gif',
] as const
export type SupportedMimeTypes = (typeof SUPPORTED_MIME_TYPES)[number]
+43 -41
View File
@@ -61,7 +61,7 @@ export function GestureActionView({
const clampedTransX = useDerivedValue(() => {
const min = actions.leftFirst ? -MAX_WIDTH : 0
const max = actions.rightFirst ? MAX_WIDTH : 0
return clamp(transX.value, min, max)
return clamp(transX.get(), min, max)
})
const iconScale = useSharedValue(1)
@@ -75,21 +75,23 @@ export function GestureActionView({
return
}
iconScale.value = withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
iconScale.set(() =>
withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
),
)
}
useAnimatedReaction(
() => transX,
() => {
if (transX.value === 0) {
if (transX.get() === 0) {
runOnJS(setActiveAction)(null)
} else if (transX.value < 0) {
} else if (transX.get() < 0) {
if (
actions.leftSecond &&
transX.value <= -actions.leftSecond.threshold
transX.get() <= -actions.leftSecond.threshold
) {
if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond')
@@ -97,10 +99,10 @@ export function GestureActionView({
} else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst')
}
} else if (transX.value > 0) {
} else if (transX.get() > 0) {
if (
actions.rightSecond &&
transX.value > actions.rightSecond.threshold
transX.get() > actions.rightSecond.threshold
) {
if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond')
@@ -119,44 +121,44 @@ export function GestureActionView({
.activeOffsetY([-200, 200])
.onStart(() => {
'worklet'
isActive.value = true
isActive.set(true)
})
.onChange(e => {
'worklet'
transX.value = e.translationX
transX.set(e.translationX)
if (e.translationX < 0) {
// Left side
if (actions.leftSecond) {
if (
e.translationX <= -actions.leftSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX > -actions.leftSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.leftFirst) {
if (!hitSecond.get() && actions.leftFirst) {
if (
e.translationX <= -actions.leftFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX > -actions.leftFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
} else if (e.translationX > 0) {
@@ -164,33 +166,33 @@ export function GestureActionView({
if (actions.rightSecond) {
if (
e.translationX >= actions.rightSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX < actions.rightSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.rightFirst) {
if (!hitSecond.get() && actions.rightFirst) {
if (
e.translationX >= actions.rightFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX < actions.rightFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
}
@@ -198,29 +200,29 @@ export function GestureActionView({
.onEnd(e => {
'worklet'
if (e.translationX < 0) {
if (hitSecond.value && actions.leftSecond) {
if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)()
} else if (hitFirst.value && actions.leftFirst) {
} else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)()
}
} else if (e.translationX > 0) {
if (hitSecond.value && actions.rightSecond) {
if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)()
} else if (hitSecond.value && actions.rightFirst) {
} else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)()
}
}
transX.value = withTiming(0, {duration: 200})
hitFirst.value = false
hitSecond.value = false
isActive.value = false
transX.set(() => withTiming(0, {duration: 200}))
hitFirst.set(false)
hitSecond.set(false)
isActive.set(false)
})
const composedGesture = Gesture.Simultaneous(panGesture)
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{translateX: clampedTransX.value}],
transform: [{translateX: clampedTransX.get()}],
}
})
@@ -274,7 +276,7 @@ export function GestureActionView({
const animatedBackgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
clampedTransX.value,
clampedTransX.get(),
interpolation.inputRange,
// @ts-expect-error - Weird type expected by reanimated, but this is okay
interpolation.outputRange,
@@ -283,10 +285,10 @@ export function GestureActionView({
})
const animatedIconStyle = useAnimatedStyle(() => {
const absTransX = Math.abs(clampedTransX.value)
const absTransX = Math.abs(clampedTransX.get())
return {
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
transform: [{scale: iconScale.value}],
transform: [{scale: iconScale.get()}],
}
})
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import Animated, {
Keyframe,
+5 -9
View File
@@ -1,8 +1,6 @@
import React from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import Animated, {
cancelAnimation,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
@@ -32,27 +30,25 @@ export function PressableScale({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
return (
<AnimatedPressable
accessibilityRole="button"
onPressIn={e => {
'worklet'
if (onPressIn) {
runOnJS(onPressIn)(e)
onPressIn(e)
}
cancelAnimation(scale)
scale.value = withTiming(targetScale, {duration: 100})
scale.set(() => withTiming(targetScale, {duration: 100}))
}}
onPressOut={e => {
'worklet'
if (onPressOut) {
runOnJS(onPressOut)(e)
onPressOut(e)
}
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}}
style={[!reducedMotion && animatedStyle, style]}
{...rest}>
+32
View File
@@ -0,0 +1,32 @@
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {BSKY_SERVICE} from '../constants'
import {getHostnameFromUrl} from '../strings/url-helpers'
export function useEmail() {
const {currentAccount} = useSession()
const {data: serviceConfig} = useServiceConfigQuery()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
// Date set for 11 AM PST on the 18th of November
const isNewEnough =
!!profile?.createdAt &&
Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
const isSelfHost =
currentAccount &&
getHostnameFromUrl(currentAccount.service) !==
getHostnameFromUrl(BSKY_SERVICE)
const needsEmailVerification =
!isSelfHost &&
checkEmailConfirmed &&
!currentAccount?.emailConfirmed &&
isNewEnough
return {needsEmailVerification}
}
+39
View File
@@ -0,0 +1,39 @@
import {useState} from 'react'
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
export type HandleRef = {
(node: any): void
current: null | number
}
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
export function useHandleRef(): HandleRef {
return useState(() => {
const ref = (node: any) => {
if (node) {
ref.current =
node._nativeTag ??
node.__nativeTag ??
node.canonical?.nativeTag ??
null
} else {
ref.current = null
}
}
ref.current = null
return ref
})[0] as HandleRef
}
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
export function measureHandle(
current: number | null,
): MeasuredDimensions | null {
'worklet'
if (current !== null) {
return measure((() => current) as AnimatedRef<any>)
} else {
return null
}
}
+11 -9
View File
@@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() {
const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => {
const headerModeValue = headerMode.get()
return {
pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerMode.value, 2),
pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerModeValue, 2),
transform: [
{
translateY: interpolate(
headerMode.value,
headerModeValue,
[0, 1],
[0, -headerHeight.value],
[0, -headerHeight.get()],
),
},
],
@@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() {
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
const footerModeValue = footerMode.get()
return {
pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerMode.value, 2),
pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerModeValue, 2),
transform: [
{
translateY: interpolate(
footerMode.value,
footerModeValue,
[0, 1],
[0, footerHeight.value],
[0, footerHeight.get()],
),
},
],
@@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() {
return {
transform: [
{
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
},
],
}
+23 -1
View File
@@ -4,12 +4,14 @@ import * as WebBrowser from 'expo-web-browser'
import {
createBskyAppAbsoluteUrl,
isBskyAppUrl,
isBskyRSSUrl,
isRelativeUrl,
} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm'
import {useTheme} from '#/alf'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
@@ -18,6 +20,7 @@ export function useOpenLink() {
const enabled = useInAppBrowser()
const t = useTheme()
const sheetWrapper = useSheetWrapper()
const optOutOfUtm = useOptOutOfUtm()
const openLink = useCallback(
async (url: string, override?: boolean) => {
@@ -26,6 +29,9 @@ export function useOpenLink() {
}
if (isNative && !url.startsWith('mailto:')) {
if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) {
url = addUtmSource(url)
}
if (override === undefined && enabled === undefined) {
openModal({
name: 'in-app-browser-consent',
@@ -47,8 +53,24 @@ export function useOpenLink() {
}
Linking.openURL(url)
},
[enabled, openModal, t, sheetWrapper],
[enabled, openModal, t, sheetWrapper, optOutOfUtm],
)
return openLink
}
function addUtmSource(url: string): string {
let parsedUrl
try {
parsedUrl = new URL(url)
} catch (e) {
return url
}
if (!parsedUrl.searchParams.has('utm_source')) {
parsedUrl.searchParams.set('utm_source', 'bluesky')
if (!parsedUrl.searchParams.has('utm_medium')) {
parsedUrl.searchParams.set('utm_medium', 'social')
}
}
return parsedUrl.toString()
}
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
+4
View File
@@ -32,6 +32,8 @@ export function mimeToExt(mimeType: SupportedMimeTypes | (string & {})) {
return 'mpeg'
case 'video/quicktime':
return 'mov'
case 'image/gif':
return 'gif'
default:
throw new Error(`Unsupported mime type: ${mimeType}`)
}
@@ -47,6 +49,8 @@ export function extToMime(ext: string) {
return 'video/mpeg'
case 'mov':
return 'video/quicktime'
case 'gif':
return 'image/gif'
default:
throw new Error(`Unsupported file extension: ${ext}`)
}
+15 -15
View File
@@ -21,11 +21,11 @@ export type LogEvents = {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
status: 'granted' | 'denied' | 'undetermined'
}
'state:background:sampled': {
'state:background': {
secondsActive: number
}
'state:foreground:sampled': {}
'router:navigate:notifications:sampled': {}
'state:foreground': {}
'router:navigate:notifications': {}
'deepLink:referrerReceived': {
to: string
referrer: string
@@ -76,7 +76,7 @@ export type LogEvents = {
'onboarding:finished:avatarResult': {
avatarResult: 'default' | 'created' | 'uploaded'
}
'home:feedDisplayed:sampled': {
'home:feedDisplayed': {
feedUrl: string
feedType: string
index: number
@@ -87,12 +87,12 @@ export type LogEvents = {
| 'desktop-sidebar-click'
| 'starter-pack-initial-feed'
}
'feed:endReached:sampled': {
'feed:endReached': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh:sampled': {
'feed:refresh': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
@@ -103,13 +103,13 @@ export type LogEvents = {
'discover:showLess': {
feedContext: string
}
'discover:clickthrough:sampled': {
'discover:clickthrough': {
count: number
}
'discover:engaged:sampled': {
'discover:engaged': {
count: number
}
'discover:seen:sampled': {
'discover:seen': {
count: number
}
@@ -132,27 +132,27 @@ export type LogEvents = {
postCount: number
isReply: boolean
}
'post:like:sampled': {
'post:like': {
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost:sampled': {
'post:repost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unlike:sampled': {
'post:unlike': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unrepost:sampled': {
'post:unrepost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:mute': {}
'post:unmute': {}
'post:pin': {}
'post:unpin': {}
'profile:follow:sampled': {
'profile:follow': {
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followerClout: number | undefined
@@ -169,7 +169,7 @@ export type LogEvents = {
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
}
'profile:unfollow:sampled': {
'profile:unfollow': {
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
-1
View File
@@ -2,4 +2,3 @@ export type Gate =
// Keep this alphabetic please.
| 'debug_show_feedcontext' // DISABLED DUE TO EME
| 'post_feed_lang_window' // DISABLED DUE TO EME
| 'suggested_feeds_interstitial'
+3 -39
View File
@@ -59,6 +59,7 @@ function createStatsigOptions(prefetchUsers: StatsigUser[]) {
initTimeoutMs: 1,
// Get fresh flags for other accounts as well, if any.
prefetchUsers,
api: 'https://events.bsky.app/v2',
}
}
@@ -89,51 +90,14 @@ export function toClout(n: number | null | undefined): number | undefined {
}
}
const DOWNSAMPLE_RATE = 0.99 // 99% likely
const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
'router:navigate:notifications:sampled',
'state:background:sampled',
'state:foreground:sampled',
'home:feedDisplayed:sampled',
'feed:endReached:sampled',
'feed:refresh:sampled',
'discover:clickthrough:sampled',
'discover:engaged:sampled',
'discover:seen:sampled',
'post:like:sampled',
'post:unlike:sampled',
'post:repost:sampled',
'post:unrepost:sampled',
'profile:follow:sampled',
'profile:unfollow:sampled',
])
const isDownsampledSession = Math.random() < DOWNSAMPLE_RATE
export function logEvent<E extends keyof LogEvents>(
eventName: E & string,
rawMetadata: LogEvents[E] & FlatJSONRecord,
) {
try {
if (
process.env.NODE_ENV === 'development' &&
eventName.endsWith(':sampled') &&
!DOWNSAMPLED_EVENTS.has(eventName)
) {
logger.error(
'Did you forget to add ' + eventName + ' to DOWNSAMPLED_EVENTS?',
)
}
const isDownsampledEvent = DOWNSAMPLED_EVENTS.has(eventName)
if (isDownsampledSession && isDownsampledEvent) {
return
}
const fullMetadata = {
...rawMetadata,
} as Record<string, string> // Statsig typings are unnecessarily strict here.
if (isDownsampledEvent) {
fullMetadata.downsampleRate = DOWNSAMPLE_RATE.toString()
}
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
if (Statsig.initializeCalled()) {
Statsig.logEvent(eventName, null, fullMetadata)
@@ -232,13 +196,13 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
lastState = state
if (state === 'active') {
lastActive = performance.now()
logEvent('state:foreground:sampled', {})
logEvent('state:foreground', {})
} else {
let secondsActive = 0
if (lastActive != null) {
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
lastActive = null
logEvent('state:background:sampled', {
logEvent('state:background', {
secondsActive,
})
}
+10 -4
View File
@@ -1,3 +1,5 @@
import {t} from '@lingui/macro'
export function cleanError(str: any): string {
if (!str) {
return ''
@@ -6,13 +8,17 @@ export function cleanError(str: any): string {
str = str.toString()
}
if (isNetworkError(str)) {
return 'Unable to connect. Please check your internet connection and try again.'
return t`Unable to connect. Please check your internet connection and try again.`
}
if (str.includes('Upstream Failure')) {
return 'The server appears to be experiencing issues. Please try again in a few moments.'
if (
str.includes('Upstream Failure') ||
str.includes('NotEnoughResources') ||
str.includes('pipethrough network error')
) {
return t`The server appears to be experiencing issues. Please try again in a few moments.`
}
if (str.includes('Bad token scope')) {
return 'This feature is not available while using an App Password. Please sign in with your main password.'
return t`This feature is not available while using an App Password. Please sign in with your main password.`
}
if (str.startsWith('Error: ')) {
return str.slice('Error: '.length)
+1
View File
@@ -11,4 +11,5 @@ test('sanitizeAppLanguageSetting', () => {
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('vi')).toBe(AppLanguage.vi)
})
+24 -3
View File
@@ -13,6 +13,12 @@ type LocalWithLanguageCode = Locale & {
*
* {@link https://github.com/bluesky-social/social-app/pull/4461}
* {@link https://xml.coverpages.org/iso639a.html}
*
* Convert Chinese language tags for Native.
*
* {@link https://datatracker.ietf.org/doc/html/rfc5646#appendix-A}
* {@link https://developer.apple.com/documentation/packagedescription/languagetag}
* {@link https://gist.github.com/amake/0ac7724681ac1c178c6f95a5b09f03ce#new-locales-vs-old-locales-chinese}
*/
export function getLocales() {
const locales = defaultGetLocales?.() ?? []
@@ -32,10 +38,25 @@ export function getLocales() {
// yiddish
locale.languageCode = 'yi'
}
// @ts-ignore checked above
output.push(locale)
}
if (typeof locale.languageTag === 'string') {
if (locale.languageTag.startsWith('zh-Hans')) {
// Simplified Chinese to zh-CN
locale.languageTag = 'zh-CN'
}
if (locale.languageTag.startsWith('zh-Hant')) {
// Traditional Chinese to zh-TW
locale.languageTag = 'zh-TW'
}
if (locale.languageTag.startsWith('yue')) {
// Cantonese (Yue) to zh-HK
locale.languageTag = 'zh-HK'
}
}
// @ts-ignore checked above
output.push(locale)
}
return output
+7 -1
View File
@@ -101,7 +101,7 @@ export function getTranslatorLink(text: string, lang: string): string {
/**
* Returns a valid `appLanguage` value from an arbitrary string.
*
* Contenxt: post-refactor, we populated some user's `appLanguage` setting with
* Context: post-refactor, we populated some user's `appLanguage` setting with
* `postLanguage`, which can be a comma-separated list of values. This breaks
* `appLanguage` handling in the app, so we introduced this util to parse out a
* valid `appLanguage` from the pre-populated `postLanguage` values.
@@ -133,6 +133,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.fr
case 'ga':
return AppLanguage.ga
case 'gl':
return AppLanguage.gl
case 'hi':
return AppLanguage.hi
case 'hu':
@@ -147,6 +149,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.ko
case 'nl':
return AppLanguage.nl
case 'pl':
return AppLanguage.pl
case 'pt-BR':
return AppLanguage.pt_BR
case 'ru':
@@ -157,6 +161,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.tr
case 'uk':
return AppLanguage.uk
case 'vi':
return AppLanguage.vi
case 'zh-CN':
return AppLanguage.zh_CN
case 'zh-HK':
+27 -1
View File
@@ -19,6 +19,7 @@ import {messages as messagesEs} from '#/locale/locales/es/messages'
import {messages as messagesFi} from '#/locale/locales/fi/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesGa} from '#/locale/locales/ga/messages'
import {messages as messagesGl} from '#/locale/locales/gl/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {messages as messagesHu} from '#/locale/locales/hu/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
@@ -26,11 +27,13 @@ import {messages as messagesIt} from '#/locale/locales/it/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesKo} from '#/locale/locales/ko/messages'
import {messages as messagesNl} from '#/locale/locales/nl/messages'
import {messages as messagesPl} from '#/locale/locales/pl/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesRu} from '#/locale/locales/ru/messages'
import {messages as messagesTh} from '#/locale/locales/th/messages'
import {messages as messagesTr} from '#/locale/locales/tr/messages'
import {messages as messagesUk} from '#/locale/locales/uk/messages'
import {messages as messagesVi} from '#/locale/locales/vi/messages'
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages'
import {messages as messagesZh_HK} from '#/locale/locales/zh-HK/messages'
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages'
@@ -97,6 +100,14 @@ export async function dynamicActivate(locale: AppLanguage) {
])
break
}
case AppLanguage.gl: {
i18n.loadAndActivate({locale, messages: messagesGl})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/gl'),
import('@formatjs/intl-numberformat/locale-data/gl'),
])
break
}
case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi})
await Promise.all([
@@ -152,6 +163,13 @@ export async function dynamicActivate(locale: AppLanguage) {
import('@formatjs/intl-numberformat/locale-data/nl'),
])
break
case AppLanguage.pl: {
i18n.loadAndActivate({locale, messages: messagesPl})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/pl'),
import('@formatjs/intl-numberformat/locale-data/pl'),
])
break
}
case AppLanguage.pt_BR: {
i18n.loadAndActivate({locale, messages: messagesPt_BR})
@@ -193,6 +211,14 @@ export async function dynamicActivate(locale: AppLanguage) {
])
break
}
case AppLanguage.vi: {
i18n.loadAndActivate({locale, messages: messagesVi})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/vi'),
import('@formatjs/intl-numberformat/locale-data/vi'),
])
break
}
case AppLanguage.zh_CN: {
i18n.loadAndActivate({locale, messages: messagesZh_CN})
await Promise.all([
@@ -224,7 +250,7 @@ export async function dynamicActivate(locale: AppLanguage) {
}
}
export async function useLocaleLanguage() {
export function useLocaleLanguage() {
const {appLanguage} = useLanguagePrefs()
useEffect(() => {
dynamicActivate(sanitizeAppLanguageSetting(appLanguage))
+12
View File
@@ -40,6 +40,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ga/messages`)
break
}
case AppLanguage.gl: {
mod = await import(`./locales/gl/messages`)
break
}
case AppLanguage.hi: {
mod = await import(`./locales/hi/messages`)
break
@@ -68,6 +72,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/nl/messages`)
break
}
case AppLanguage.pl: {
mod = await import(`./locales/pl/messages`)
break
}
case AppLanguage.pt_BR: {
mod = await import(`./locales/pt-BR/messages`)
break
@@ -88,6 +96,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/uk/messages`)
break
}
case AppLanguage.vi: {
mod = await import(`./locales/vi/messages`)
break
}
case AppLanguage.zh_CN: {
mod = await import(`./locales/zh-CN/messages`)
break
+8 -2
View File
@@ -13,6 +13,7 @@ export enum AppLanguage {
fi = 'fi',
fr = 'fr',
ga = 'ga',
gl = 'gl',
hi = 'hi',
hu = 'hu',
id = 'id',
@@ -20,11 +21,13 @@ export enum AppLanguage {
ja = 'ja',
ko = 'ko',
nl = 'nl',
pl = 'pl',
pt_BR = 'pt-BR',
ru = 'ru',
th = 'th',
tr = 'tr',
uk = 'uk',
vi = 'vi',
zh_CN = 'zh-CN',
zh_HK = 'zh-HK',
zh_TW = 'zh-TW',
@@ -44,6 +47,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.fi, name: 'Suomi Finnish'},
{code2: AppLanguage.fr, name: 'Français French'},
{code2: AppLanguage.ga, name: 'Gaeilge Irish'},
{code2: AppLanguage.gl, name: 'Galego - Galician'},
{code2: AppLanguage.hi, name: 'हिंदी Hindi'},
{code2: AppLanguage.hu, name: 'magyar Hungarian'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia Indonesian'},
@@ -51,11 +55,13 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.ja, name: '日本語 Japanese'},
{code2: AppLanguage.ko, name: '한국어 Korean'},
{code2: AppLanguage.nl, name: 'Nederlands Dutch'},
{code2: AppLanguage.pl, name: 'Polski Polish'},
{code2: AppLanguage.pt_BR, name: 'Português (BR) Portuguese (BR)'},
{code2: AppLanguage.ru, name: 'Русский Russian'},
{code2: AppLanguage.th, name: 'ภาษาไทย Thai'},
{code2: AppLanguage.tr, name: 'Türkçe Turkish'},
{code2: AppLanguage.uk, name: 'Українська Ukrainian'},
{code2: AppLanguage.vi, name: 'Tiếng Việt Vietnamese'},
{code2: AppLanguage.zh_CN, name: '简体中文 Simplified Chinese'},
{code2: AppLanguage.zh_TW, name: '繁體中文 Traditional Chinese'},
{code2: AppLanguage.zh_HK, name: '粵文 Cantonese'},
@@ -469,8 +475,8 @@ export const LANGUAGES: Language[] = [
{code3: 'roa', code2: '', name: 'Romance languages'},
{code3: 'roh', code2: 'rm', name: 'Romansh'},
{code3: 'rom', code2: '', name: 'Romany'},
{code3: 'rum', code2: 'ro', name: 'Romanian; Moldavian; Moldovan'},
{code3: 'ron', code2: 'ro', name: 'Romanian; Moldavian; Moldovan'},
{code3: 'rum', code2: 'ro', name: 'Romanian'},
{code3: 'ron', code2: 'ro', name: 'Romanian'},
{code3: 'run', code2: 'rn', name: 'Rundi'},
{code3: 'rup', code2: '', name: 'Aromanian; Arumanian; Macedo-Romanian'},
{code3: 'rus', code2: 'ru', name: 'Russian'},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More