Merge branch 'main' into main
This commit is contained in:
+37
-34
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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(() => {})
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {atoms as a, tokens} from '#/alf'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import * as Progress from 'react-native-progress'
|
||||
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export function SubtleWebHover({}: {hover: boolean}) {
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
|
||||
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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,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,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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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,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,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,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,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'
|
||||
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {
|
||||
Keyframe,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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]),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
|
||||
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'},
|
||||
|
||||
+1539
-976
File diff suppressed because it is too large
Load Diff
+1499
-928
File diff suppressed because it is too large
Load Diff
+1498
-891
File diff suppressed because it is too large
Load Diff
+1497
-890
File diff suppressed because it is too large
Load Diff
+1637
-1071
File diff suppressed because it is too large
Load Diff
+1500
-929
File diff suppressed because it is too large
Load Diff
+1628
-1293
File diff suppressed because it is too large
Load Diff
+1811
-2380
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3051
-3805
File diff suppressed because it is too large
Load Diff
+2744
-1819
File diff suppressed because it is too large
Load Diff
+1502
-931
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
Reference in New Issue
Block a user