Merge branch 'bluesky-social:main' into patch-3
commit
236223d02a
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 738 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 371 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 717 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 508 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 503 B |
|
@ -43,6 +43,9 @@
|
||||||
height: calc(100% + env(safe-area-inset-top));
|
height: calc(100% + env(safe-area-inset-top));
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
html, body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||||
button, input, textarea {
|
button, input, textarea {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import '#/platform/markBundleStartTime'
|
||||||
|
|
||||||
import '#/platform/polyfills'
|
import '#/platform/polyfills'
|
||||||
import {registerRootComponent} from 'expo'
|
import {registerRootComponent} from 'expo'
|
||||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||||
|
|
|
@ -78,6 +78,7 @@ import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStack
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {i18n, MessageDescriptor} from '@lingui/core'
|
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
|
import {logEvent} from './lib/statsig/statsig'
|
||||||
|
|
||||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||||
|
|
||||||
|
@ -649,11 +650,14 @@ function logModuleInitTime() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
didInit = true
|
didInit = true
|
||||||
|
|
||||||
const initMs = Math.round(
|
const initMs = Math.round(
|
||||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||||
performance.now() - global.__BUNDLE_START_TIME__,
|
performance.now() - global.__BUNDLE_START_TIME__,
|
||||||
)
|
)
|
||||||
console.log(`Time to first paint: ${initMs} ms`)
|
console.log(`Time to first paint: ${initMs} ms`)
|
||||||
|
logEvent('init', initMs)
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// This log is noisy, so keep false committed
|
// This log is noisy, so keep false committed
|
||||||
const shouldLog = false
|
const shouldLog = false
|
||||||
|
|
|
@ -27,7 +27,7 @@ export type ButtonColor =
|
||||||
| 'gradient_sunset'
|
| 'gradient_sunset'
|
||||||
| 'gradient_nordic'
|
| 'gradient_nordic'
|
||||||
| 'gradient_bonfire'
|
| 'gradient_bonfire'
|
||||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
|
||||||
export type ButtonShape = 'round' | 'square' | 'default'
|
export type ButtonShape = 'round' | 'square' | 'default'
|
||||||
export type VariantProps = {
|
export type VariantProps = {
|
||||||
/**
|
/**
|
||||||
|
@ -274,6 +274,8 @@ export function Button({
|
||||||
if (shape === 'default') {
|
if (shape === 'default') {
|
||||||
if (size === 'large') {
|
if (size === 'large') {
|
||||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||||
|
} else if (size === 'medium') {
|
||||||
|
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||||
} else if (size === 'small') {
|
} else if (size === 'small') {
|
||||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||||
} else if (size === 'tiny') {
|
} else if (size === 'tiny') {
|
||||||
|
|
|
@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||||
open: () => {},
|
open: () => {},
|
||||||
close: () => {},
|
close: () => {},
|
||||||
})
|
})
|
||||||
const {activeDialogs, openDialogs} = useDialogStateContext()
|
const {activeDialogs} = useDialogStateContext()
|
||||||
const isOpen = openDialogs.includes(id)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
activeDialogs.current.set(id, control)
|
activeDialogs.current.set(id, control)
|
||||||
|
@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||||
() => ({
|
() => ({
|
||||||
id,
|
id,
|
||||||
ref: control,
|
ref: control,
|
||||||
isOpen,
|
|
||||||
open: () => {
|
open: () => {
|
||||||
control.current.open()
|
control.current.open()
|
||||||
},
|
},
|
||||||
|
@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
||||||
control.current.close(cb)
|
control.current.close(cb)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[id, control, isOpen],
|
[id, control],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export type DialogControlRefProps = {
|
||||||
export type DialogControlProps = DialogControlRefProps & {
|
export type DialogControlProps = DialogControlRefProps & {
|
||||||
id: string
|
id: string
|
||||||
ref: React.RefObject<DialogControlRefProps>
|
ref: React.RefObject<DialogControlRefProps>
|
||||||
isOpen: boolean
|
isOpen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DialogContextProps = {
|
export type DialogContextProps = {
|
||||||
|
|
|
@ -92,10 +92,8 @@ export function Trigger({children, label, style}: TriggerProps) {
|
||||||
accessibilityLabel={label}
|
accessibilityLabel={label}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
style={flatten([style, web({outline: 0})])}
|
style={flatten([style, focused && web({outline: 0})])}
|
||||||
onPointerDown={() => {
|
onPointerDown={() => control.open()}
|
||||||
control.open()
|
|
||||||
}}
|
|
||||||
{...web({
|
{...web({
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
|
@ -131,6 +129,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Disabled until we can fix positioning
|
||||||
<DropdownMenu.Arrow
|
<DropdownMenu.Arrow
|
||||||
className="DropdownMenuArrow"
|
className="DropdownMenuArrow"
|
||||||
fill={
|
fill={
|
||||||
|
@ -138,6 +137,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
||||||
.backgroundColor
|
.backgroundColor
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ export function Outer({
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
}>) {
|
}>) {
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const titleId = React.useId()
|
const titleId = React.useId()
|
||||||
const descriptionId = React.useId()
|
const descriptionId = React.useId()
|
||||||
|
|
||||||
|
@ -38,12 +39,12 @@ export function Outer({
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|
||||||
<Dialog.Inner
|
<Dialog.ScrollableInner
|
||||||
accessibilityLabelledBy={titleId}
|
accessibilityLabelledBy={titleId}
|
||||||
accessibilityDescribedBy={descriptionId}
|
accessibilityDescribedBy={descriptionId}
|
||||||
style={[{width: 'auto', maxWidth: 400}]}>
|
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Inner>
|
</Dialog.ScrollableInner>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
@ -71,8 +72,16 @@ export function Description({children}: React.PropsWithChildren<{}>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Actions({children}: React.PropsWithChildren<{}>) {
|
export function Actions({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.gap_sm,
|
||||||
|
a.justify_end,
|
||||||
|
gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl],
|
||||||
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -82,12 +91,13 @@ export function Cancel({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
|
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const {close} = Dialog.useDialogContext()
|
const {close} = Dialog.useDialogContext()
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size={gtMobile ? 'small' : 'medium'}
|
||||||
label={_(msg`Cancel`)}
|
label={_(msg`Cancel`)}
|
||||||
onPress={() => close()}>
|
onPress={() => close()}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -100,6 +110,7 @@ export function Action({
|
||||||
onPress,
|
onPress,
|
||||||
}: React.PropsWithChildren<{onPress?: () => void}>) {
|
}: React.PropsWithChildren<{onPress?: () => void}>) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const {close} = Dialog.useDialogContext()
|
const {close} = Dialog.useDialogContext()
|
||||||
const handleOnPress = React.useCallback(() => {
|
const handleOnPress = React.useCallback(() => {
|
||||||
close()
|
close()
|
||||||
|
@ -109,7 +120,7 @@ export function Action({
|
||||||
<Button
|
<Button
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size={gtMobile ? 'small' : 'medium'}
|
||||||
label={_(msg`Confirm`)}
|
label={_(msg`Confirm`)}
|
||||||
onPress={handleOnPress}>
|
onPress={handleOnPress}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -98,7 +98,7 @@ export function TagMenu({
|
||||||
|
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
navigation.push('Hashtag', {
|
navigation.push('Hashtag', {
|
||||||
tag: tag.replaceAll('#', '%23'),
|
tag: encodeURIComponent(tag),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ export function TagMenu({
|
||||||
|
|
||||||
control.close(() => {
|
control.close(() => {
|
||||||
navigation.push('Hashtag', {
|
navigation.push('Hashtag', {
|
||||||
tag: tag.replaceAll('#', '%23'),
|
tag: encodeURIComponent(tag),
|
||||||
author: authorHandle,
|
author: authorHandle,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -66,7 +66,7 @@ export function TagMenu({
|
||||||
label: _(msg`See ${truncatedTag} posts`),
|
label: _(msg`See ${truncatedTag} posts`),
|
||||||
onPress() {
|
onPress() {
|
||||||
navigation.push('Hashtag', {
|
navigation.push('Hashtag', {
|
||||||
tag: tag.replaceAll('#', '%23'),
|
tag: encodeURIComponent(tag),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
testID: 'tagMenuSearch',
|
testID: 'tagMenuSearch',
|
||||||
|
@ -83,7 +83,7 @@ export function TagMenu({
|
||||||
label: _(msg`See ${truncatedTag} posts by user`),
|
label: _(msg`See ${truncatedTag} posts by user`),
|
||||||
onPress() {
|
onPress() {
|
||||||
navigation.push('Hashtag', {
|
navigation.push('Hashtag', {
|
||||||
tag: tag.replaceAll('#', '%23'),
|
tag: encodeURIComponent(tag),
|
||||||
author: authorHandle,
|
author: authorHandle,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Filter_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const SpeakerVolumeFull_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Trash_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Warning_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z',
|
||||||
|
})
|
|
@ -2,9 +2,15 @@ import {RouteParams, Route} from './types'
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
routes: [string, Route][] = []
|
routes: [string, Route][] = []
|
||||||
constructor(description: Record<string, string>) {
|
constructor(description: Record<string, string | string[]>) {
|
||||||
for (const [screen, pattern] of Object.entries(description)) {
|
for (const [screen, pattern] of Object.entries(description)) {
|
||||||
|
if (typeof pattern === 'string') {
|
||||||
this.routes.push([screen, createRoute(pattern)])
|
this.routes.push([screen, createRoute(pattern)])
|
||||||
|
} else {
|
||||||
|
pattern.forEach(subPattern => {
|
||||||
|
this.routes.push([screen, createRoute(subPattern)])
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,75 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Statsig,
|
||||||
|
StatsigProvider,
|
||||||
|
useGate as useStatsigGate,
|
||||||
|
} from 'statsig-react-native-expo'
|
||||||
|
import {useSession} from '../../state/session'
|
||||||
|
import {sha256} from 'js-sha256'
|
||||||
|
|
||||||
export function useGate(_gateName: string) {
|
const statsigOptions = {
|
||||||
// Not enabled for native yet.
|
environment: {
|
||||||
return false
|
tier: process.env.NODE_ENV === 'development' ? 'development' : 'production',
|
||||||
|
},
|
||||||
|
// Don't block on waiting for network. The fetched config will kick in on next load.
|
||||||
|
// This ensures the UI is always consistent and doesn't update mid-session.
|
||||||
|
// Note this makes cold load (no local storage) and private mode return `false` for all gates.
|
||||||
|
initTimeoutMs: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logEvent(
|
||||||
|
eventName: string,
|
||||||
|
value?: string | number | null,
|
||||||
|
metadata?: Record<string, string> | null,
|
||||||
|
) {
|
||||||
|
Statsig.logEvent(eventName, value, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGate(gateName: string) {
|
||||||
|
const {isLoading, value} = useStatsigGate(gateName)
|
||||||
|
if (isLoading) {
|
||||||
|
// This should not happen because of waitForInitialization={true}.
|
||||||
|
console.error('Did not expected isLoading to ever be true.')
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStatsigUser(did: string | undefined) {
|
||||||
|
let userID: string | undefined
|
||||||
|
if (did) {
|
||||||
|
userID = sha256(did)
|
||||||
|
}
|
||||||
|
return {userID}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
// Not enabled for native yet.
|
const {currentAccount} = useSession()
|
||||||
return children
|
const currentStatsigUser = React.useMemo(
|
||||||
|
() => toStatsigUser(currentAccount?.did),
|
||||||
|
[currentAccount?.did],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function refresh() {
|
||||||
|
// Intentionally refetching the config using the JS SDK rather than React SDK
|
||||||
|
// so that the new config is stored in cache but isn't used during this session.
|
||||||
|
// It will kick in for the next reload.
|
||||||
|
Statsig.updateUser(currentStatsigUser)
|
||||||
|
}
|
||||||
|
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [currentStatsigUser])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsigProvider
|
||||||
|
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
|
||||||
|
mountKey={currentStatsigUser.userID}
|
||||||
|
user={currentStatsigUser}
|
||||||
|
// This isn't really blocking due to short initTimeoutMs above.
|
||||||
|
// However, it ensures `isLoading` is always `false`.
|
||||||
|
waitForInitialization={true}
|
||||||
|
options={statsigOptions}>
|
||||||
|
{children}
|
||||||
|
</StatsigProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StatsigProvider, useGate as useStatsigGate} from 'statsig-react'
|
import {
|
||||||
|
Statsig,
|
||||||
|
StatsigProvider,
|
||||||
|
useGate as useStatsigGate,
|
||||||
|
} from 'statsig-react'
|
||||||
import {useSession} from '../../state/session'
|
import {useSession} from '../../state/session'
|
||||||
import {sha256} from 'js-sha256'
|
import {sha256} from 'js-sha256'
|
||||||
|
|
||||||
|
@ -13,6 +17,14 @@ const statsigOptions = {
|
||||||
initTimeoutMs: 1,
|
initTimeoutMs: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logEvent(
|
||||||
|
eventName: string,
|
||||||
|
value?: string | number | null,
|
||||||
|
metadata?: Record<string, string> | null,
|
||||||
|
) {
|
||||||
|
Statsig.logEvent(eventName, value, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
export function useGate(gateName: string) {
|
export function useGate(gateName: string) {
|
||||||
const {isLoading, value} = useStatsigGate(gateName)
|
const {isLoading, value} = useStatsigGate(gateName)
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@ -36,6 +48,18 @@ export function Provider({children}: {children: React.ReactNode}) {
|
||||||
() => toStatsigUser(currentAccount?.did),
|
() => toStatsigUser(currentAccount?.did),
|
||||||
[currentAccount?.did],
|
[currentAccount?.did],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function refresh() {
|
||||||
|
// Intentionally refetching the config using the JS SDK rather than React SDK
|
||||||
|
// so that the new config is stored in cache but isn't used during this session.
|
||||||
|
// It will kick in for the next reload.
|
||||||
|
Statsig.updateUser(currentStatsigUser)
|
||||||
|
}
|
||||||
|
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [currentStatsigUser])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsigProvider
|
<StatsigProvider
|
||||||
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
|
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {BSKY_SERVICE} from 'lib/constants'
|
||||||
import TLDs from 'tlds'
|
import TLDs from 'tlds'
|
||||||
import psl from 'psl'
|
import psl from 'psl'
|
||||||
|
|
||||||
|
export const BSKY_APP_HOST = 'https://bsky.app'
|
||||||
|
|
||||||
export function isValidDomain(str: string): boolean {
|
export function isValidDomain(str: string): boolean {
|
||||||
return !!TLDs.find(tld => {
|
return !!TLDs.find(tld => {
|
||||||
let i = str.lastIndexOf(tld)
|
let i = str.lastIndexOf(tld)
|
||||||
|
@ -67,8 +69,21 @@ export function isBskyAppUrl(url: string): boolean {
|
||||||
return url.startsWith('https://bsky.app/')
|
return url.startsWith('https://bsky.app/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isRelativeUrl(url: string): boolean {
|
||||||
|
return /^\/[^/]/.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBskyRSSUrl(url: string): boolean {
|
||||||
|
return (
|
||||||
|
(url.startsWith('https://bsky.app/') || isRelativeUrl(url)) &&
|
||||||
|
/\/rss\/?$/.test(url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function isExternalUrl(url: string): boolean {
|
export function isExternalUrl(url: string): boolean {
|
||||||
return !isBskyAppUrl(url) && url.startsWith('http')
|
const external = !isBskyAppUrl(url) && url.startsWith('http')
|
||||||
|
const rss = isBskyRSSUrl(url)
|
||||||
|
return external || rss
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBskyPostUrl(url: string): boolean {
|
export function isBskyPostUrl(url: string): boolean {
|
||||||
|
@ -149,7 +164,7 @@ export function linkRequiresWarning(uri: string, label: string) {
|
||||||
const labelDomain = labelToDomain(label)
|
const labelDomain = labelToDomain(label)
|
||||||
|
|
||||||
// If the uri started with a / we know it is internal.
|
// If the uri started with a / we know it is internal.
|
||||||
if (uri.startsWith('/')) {
|
if (isRelativeUrl(uri)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,3 +237,8 @@ export function splitApexDomain(hostname: string): [string, string] {
|
||||||
hostnamep.domain,
|
hostnamep.domain,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createBskyAppAbsoluteUrl(path: string): string {
|
||||||
|
const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '')
|
||||||
|
return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}`
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
// @ts-ignore Web-only. On RN, this is set by Metro.
|
||||||
|
window.__BUNDLE_START_TIME__ = performance.now()
|
|
@ -12,7 +12,7 @@ export const router = new Router({
|
||||||
ModerationModlists: '/moderation/modlists',
|
ModerationModlists: '/moderation/modlists',
|
||||||
ModerationMutedAccounts: '/moderation/muted-accounts',
|
ModerationMutedAccounts: '/moderation/muted-accounts',
|
||||||
ModerationBlockedAccounts: '/moderation/blocked-accounts',
|
ModerationBlockedAccounts: '/moderation/blocked-accounts',
|
||||||
Profile: '/profile/:name',
|
Profile: ['/profile/:name', '/profile/:name/rss'],
|
||||||
ProfileFollowers: '/profile/:name/followers',
|
ProfileFollowers: '/profile/:name/followers',
|
||||||
ProfileFollows: '/profile/:name/follows',
|
ProfileFollows: '/profile/:name/follows',
|
||||||
ProfileList: '/profile/:name/lists/:rkey',
|
ProfileList: '/profile/:name/lists/:rkey',
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default function HashtagScreen({
|
||||||
const [isPTR, setIsPTR] = React.useState(false)
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
|
|
||||||
const fullTag = React.useMemo(() => {
|
const fullTag = React.useMemo(() => {
|
||||||
return `#${tag.replaceAll('%23', '#')}`
|
return `#${decodeURIComponent(tag)}`
|
||||||
}, [tag])
|
}, [tag])
|
||||||
|
|
||||||
const queryParam = React.useMemo(() => {
|
const queryParam = React.useMemo(() => {
|
||||||
|
@ -83,7 +83,7 @@ export default function HashtagScreen({
|
||||||
|
|
||||||
const onShare = React.useCallback(() => {
|
const onShare = React.useCallback(() => {
|
||||||
const url = new URL('https://bsky.app')
|
const url = new URL('https://bsky.app')
|
||||||
url.pathname = `/hashtag/${tag}`
|
url.pathname = `/hashtag/${decodeURIComponent(tag)}`
|
||||||
if (author) {
|
if (author) {
|
||||||
url.searchParams.set('author', author)
|
url.searchParams.set('author', author)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
||||||
import {DialogControlRefProps} from '#/components/Dialog'
|
import {DialogControlRefProps} from '#/components/Dialog'
|
||||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||||
|
|
||||||
const DialogContext = React.createContext<{
|
interface IDialogContext {
|
||||||
/**
|
/**
|
||||||
* The currently active `useDialogControl` hooks.
|
* The currently active `useDialogControl` hooks.
|
||||||
*/
|
*/
|
||||||
|
@ -13,13 +14,18 @@ const DialogContext = React.createContext<{
|
||||||
* The currently open dialogs, referenced by their IDs, generated from
|
* The currently open dialogs, referenced by their IDs, generated from
|
||||||
* `useId`.
|
* `useId`.
|
||||||
*/
|
*/
|
||||||
openDialogs: string[]
|
openDialogs: React.MutableRefObject<Set<string>>
|
||||||
}>({
|
/**
|
||||||
activeDialogs: {
|
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
||||||
current: new Map(),
|
* applies to the parent of all non-modal views, and prevents TalkBack from
|
||||||
},
|
* navigating within content beneath an open dialog.
|
||||||
openDialogs: [],
|
*
|
||||||
})
|
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
||||||
|
*/
|
||||||
|
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
||||||
|
|
||||||
const DialogControlContext = React.createContext<{
|
const DialogControlContext = React.createContext<{
|
||||||
closeAllDialogs(): boolean
|
closeAllDialogs(): boolean
|
||||||
|
@ -41,26 +47,42 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const activeDialogs = React.useRef<
|
const activeDialogs = React.useRef<
|
||||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||||
>(new Map())
|
>(new Map())
|
||||||
const [openDialogs, setOpenDialogs] = React.useState<string[]>([])
|
const openDialogs = React.useRef<Set<string>>(new Set())
|
||||||
|
const importantForAccessibility = useSharedValue<
|
||||||
|
'auto' | 'no-hide-descendants'
|
||||||
|
>('auto')
|
||||||
|
|
||||||
const closeAllDialogs = React.useCallback(() => {
|
const closeAllDialogs = React.useCallback(() => {
|
||||||
activeDialogs.current.forEach(dialog => dialog.current.close())
|
activeDialogs.current.forEach(dialog => dialog.current.close())
|
||||||
return openDialogs.length > 0
|
return openDialogs.current.size > 0
|
||||||
}, [openDialogs])
|
}, [])
|
||||||
|
|
||||||
const setDialogIsOpen = React.useCallback(
|
const setDialogIsOpen = React.useCallback(
|
||||||
(id: string, isOpen: boolean) => {
|
(id: string, isOpen: boolean) => {
|
||||||
setOpenDialogs(prev => {
|
if (isOpen) {
|
||||||
const filtered = prev.filter(dialogId => dialogId !== id) as string[]
|
openDialogs.current.add(id)
|
||||||
return isOpen ? [...filtered, id] : filtered
|
importantForAccessibility.value = 'no-hide-descendants'
|
||||||
})
|
} else {
|
||||||
|
openDialogs.current.delete(id)
|
||||||
|
if (openDialogs.current.size < 1) {
|
||||||
|
importantForAccessibility.value = 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setOpenDialogs],
|
[importantForAccessibility],
|
||||||
)
|
)
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = React.useMemo<IDialogContext>(
|
||||||
() => ({activeDialogs, openDialogs}),
|
() => ({
|
||||||
[openDialogs],
|
activeDialogs: {
|
||||||
|
current: new Map(),
|
||||||
|
},
|
||||||
|
openDialogs: {
|
||||||
|
current: new Set(),
|
||||||
|
},
|
||||||
|
importantForAccessibility,
|
||||||
|
}),
|
||||||
|
[importantForAccessibility],
|
||||||
)
|
)
|
||||||
const controls = React.useMemo(
|
const controls = React.useMemo(
|
||||||
() => ({closeAllDialogs, setDialogIsOpen}),
|
() => ({closeAllDialogs, setDialogIsOpen}),
|
||||||
|
|
|
@ -5,6 +5,11 @@ import * as WebBrowser from 'expo-web-browser'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useModalControls} from '../modals'
|
import {useModalControls} from '../modals'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {
|
||||||
|
isBskyRSSUrl,
|
||||||
|
isRelativeUrl,
|
||||||
|
createBskyAppAbsoluteUrl,
|
||||||
|
} from 'lib/strings/url-helpers'
|
||||||
|
|
||||||
type StateContext = persisted.Schema['useInAppBrowser']
|
type StateContext = persisted.Schema['useInAppBrowser']
|
||||||
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
|
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
|
||||||
|
@ -57,6 +62,10 @@ export function useOpenLink() {
|
||||||
|
|
||||||
const openLink = React.useCallback(
|
const openLink = React.useCallback(
|
||||||
(url: string, override?: boolean) => {
|
(url: string, override?: boolean) => {
|
||||||
|
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
|
||||||
|
url = createBskyAppAbsoluteUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
if (isNative && !url.startsWith('mailto:')) {
|
if (isNative && !url.startsWith('mailto:')) {
|
||||||
if (override === undefined && enabled === undefined) {
|
if (override === undefined && enabled === undefined) {
|
||||||
openModal({
|
openModal({
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Keyboard,
|
Keyboard,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {CreateAccountState, CreateAccountDispatch, is18} from './state'
|
import {CreateAccountState, CreateAccountDispatch, is18} from './state'
|
||||||
|
@ -19,7 +18,6 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
|
@ -49,7 +47,6 @@ export function Step1({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const serverInputControl = useDialogControl()
|
const serverInputControl = useDialogControl()
|
||||||
|
|
||||||
const onPressSelectService = React.useCallback(() => {
|
const onPressSelectService = React.useCallback(() => {
|
||||||
|
@ -57,10 +54,6 @@ export function Step1({
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
}, [serverInputControl])
|
}, [serverInputControl])
|
||||||
|
|
||||||
const onPressWaitlist = React.useCallback(() => {
|
|
||||||
openModal({name: 'waitlist'})
|
|
||||||
}, [openModal])
|
|
||||||
|
|
||||||
const birthDate = React.useMemo(() => {
|
const birthDate = React.useMemo(() => {
|
||||||
return sanitizeDate(uiState.birthDate)
|
return sanitizeDate(uiState.birthDate)
|
||||||
}, [uiState.birthDate])
|
}, [uiState.birthDate])
|
||||||
|
@ -164,23 +157,7 @@ export function Step1({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!uiState.inviteCode && uiState.isInviteCodeRequired ? (
|
{!uiState.isInviteCodeRequired || uiState.inviteCode ? (
|
||||||
<View style={[s.flexRow, s.alignCenter]}>
|
|
||||||
<Text style={pal.text}>
|
|
||||||
<Trans>Don't have an invite code?</Trans>{' '}
|
|
||||||
</Text>
|
|
||||||
<TouchableWithoutFeedback
|
|
||||||
onPress={onPressWaitlist}
|
|
||||||
accessibilityLabel={_(msg`Join the waitlist.`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<View style={styles.touchable}>
|
|
||||||
<Text style={pal.link}>
|
|
||||||
<Trans>Join the waitlist.</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<View style={s.pb20}>
|
<View style={s.pb20}>
|
||||||
<Text
|
<Text
|
||||||
|
@ -260,7 +237,7 @@ export function Step1({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -20,7 +20,6 @@ import * as ReportModal from './report/Modal'
|
||||||
import * as AppealLabelModal from './AppealLabel'
|
import * as AppealLabelModal from './AppealLabel'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
import * as WaitlistModal from './Waitlist'
|
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as AddAppPassword from './AddAppPasswords'
|
import * as AddAppPassword from './AddAppPasswords'
|
||||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||||
|
@ -109,9 +108,6 @@ export function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'change-handle') {
|
} else if (activeModal?.name === 'change-handle') {
|
||||||
snapPoints = ChangeHandleModal.snapPoints
|
snapPoints = ChangeHandleModal.snapPoints
|
||||||
element = <ChangeHandleModal.Component {...activeModal} />
|
element = <ChangeHandleModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'waitlist') {
|
|
||||||
snapPoints = WaitlistModal.snapPoints
|
|
||||||
element = <WaitlistModal.Component />
|
|
||||||
} else if (activeModal?.name === 'invite-codes') {
|
} else if (activeModal?.name === 'invite-codes') {
|
||||||
snapPoints = InviteCodesModal.snapPoints
|
snapPoints = InviteCodesModal.snapPoints
|
||||||
element = <InviteCodesModal.Component />
|
element = <InviteCodesModal.Component />
|
||||||
|
|
|
@ -22,7 +22,6 @@ import * as CropImageModal from './crop-image/CropImage.web'
|
||||||
import * as AltTextImageModal from './AltImage'
|
import * as AltTextImageModal from './AltImage'
|
||||||
import * as EditImageModal from './EditImage'
|
import * as EditImageModal from './EditImage'
|
||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
import * as WaitlistModal from './Waitlist'
|
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as AddAppPassword from './AddAppPasswords'
|
import * as AddAppPassword from './AddAppPasswords'
|
||||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||||
|
@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <ThreadgateModal.Component {...modal} />
|
element = <ThreadgateModal.Component {...modal} />
|
||||||
} else if (modal.name === 'change-handle') {
|
} else if (modal.name === 'change-handle') {
|
||||||
element = <ChangeHandleModal.Component {...modal} />
|
element = <ChangeHandleModal.Component {...modal} />
|
||||||
} else if (modal.name === 'waitlist') {
|
|
||||||
element = <WaitlistModal.Component />
|
|
||||||
} else if (modal.name === 'invite-codes') {
|
} else if (modal.name === 'invite-codes') {
|
||||||
element = <InviteCodesModal.Component />
|
element = <InviteCodesModal.Component />
|
||||||
} else if (modal.name === 'add-app-password') {
|
} else if (modal.name === 'add-app-password') {
|
||||||
|
|
|
@ -1,190 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {TextInput} from './util'
|
|
||||||
import {
|
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconStyle,
|
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {s, gradients} from 'lib/styles'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
|
||||||
|
|
||||||
export function Component({}: {}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const [email, setEmail] = React.useState<string>('')
|
|
||||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
|
||||||
const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
|
|
||||||
const [error, setError] = React.useState<string>('')
|
|
||||||
|
|
||||||
const onPressSignup = async () => {
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
const res = await fetch('https://bsky.app/api/waitlist', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({email}),
|
|
||||||
})
|
|
||||||
const resBody = await res.json()
|
|
||||||
if (resBody.success) {
|
|
||||||
setIsEmailSent(true)
|
|
||||||
} else {
|
|
||||||
setError(
|
|
||||||
resBody.error ||
|
|
||||||
_(msg`Something went wrong. Check your email and try again.`),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(cleanError(e))
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
const onCancel = () => {
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, pal.view]}>
|
|
||||||
<View style={[styles.innerContainer, pal.view]}>
|
|
||||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
|
||||||
<Trans>Join the waitlist</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[styles.description, pal.text]}>
|
|
||||||
<Trans>
|
|
||||||
Bluesky uses invites to build a healthier community. If you don't
|
|
||||||
know anybody with an invite, you can sign up for the waitlist and
|
|
||||||
we'll send one soon.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
|
|
||||||
placeholder={_(msg`Enter your email`)}
|
|
||||||
placeholderTextColor={pal.textLight.color}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
|
||||||
onSubmitEditing={onPressSignup}
|
|
||||||
enterKeyHint="done"
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel={_(msg`Email`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Input your email to get on the Bluesky waitlist`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<View style={s.mt10}>
|
|
||||||
<ErrorMessage message={error} style={styles.error} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{isProcessing ? (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : isEmailSent ? (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="check"
|
|
||||||
style={pal.text as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
<Text style={[s.ml10, pal.text]}>
|
|
||||||
<Trans>
|
|
||||||
Your email has been saved! We'll be in touch soon.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressSignup}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Confirms signing up ${email} to the waitlist`,
|
|
||||||
)}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
|
||||||
<Trans>Join Waitlist</Trans>
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.btn, s.mt10]}
|
|
||||||
onPress={onCancel}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Cancel waitlist signup`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Exits signing up for waitlist with ${email}`,
|
|
||||||
)}
|
|
||||||
onAccessibilityEscape={onCancel}>
|
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
innerContainer: {
|
|
||||||
paddingBottom: 20,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingHorizontal: 22,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
fontSize: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
borderRadius: 6,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native'
|
||||||
export function EventStopper({
|
export function EventStopper({
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) {
|
onKeyDown = true,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
style?: ViewStyle | ViewStyle[]
|
||||||
|
/**
|
||||||
|
* Default `true`. Set to `false` to allow onKeyDown to propagate
|
||||||
|
*/
|
||||||
|
onKeyDown?: boolean
|
||||||
|
}>) {
|
||||||
const stop = (e: any) => {
|
const stop = (e: any) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}
|
}
|
||||||
|
@ -18,7 +25,7 @@ export function EventStopper({
|
||||||
onTouchEnd={stop}
|
onTouchEnd={stop}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
onClick={stop}
|
onClick={stop}
|
||||||
onKeyDown={stop}
|
onKeyDown={onKeyDown ? stop : undefined}
|
||||||
style={style}>
|
style={style}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
import {
|
||||||
|
StyleProp,
|
||||||
|
ViewStyle,
|
||||||
|
Pressable,
|
||||||
|
View,
|
||||||
|
PressableProps,
|
||||||
|
} from 'react-native'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
@ -12,10 +18,6 @@ import {
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from 'lib/sharing'
|
||||||
import {
|
|
||||||
NativeDropdown,
|
|
||||||
DropdownItem as NativeDropdownItem,
|
|
||||||
} from './NativeDropdown'
|
|
||||||
import * as Toast from '../Toast'
|
import * as Toast from '../Toast'
|
||||||
import {EventStopper} from '../EventStopper'
|
import {EventStopper} from '../EventStopper'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
@ -36,6 +38,19 @@ import {isWeb} from '#/platform/detection'
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme as useAlf, web} from '#/alf'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||||
|
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
|
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||||
|
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||||
|
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
|
||||||
let PostDropdownBtn = ({
|
let PostDropdownBtn = ({
|
||||||
testID,
|
testID,
|
||||||
postAuthor,
|
postAuthor,
|
||||||
|
@ -45,6 +60,7 @@ let PostDropdownBtn = ({
|
||||||
richText,
|
richText,
|
||||||
style,
|
style,
|
||||||
showAppealLabelItem,
|
showAppealLabelItem,
|
||||||
|
hitSlop,
|
||||||
}: {
|
}: {
|
||||||
testID: string
|
testID: string
|
||||||
postAuthor: AppBskyActorDefs.ProfileViewBasic
|
postAuthor: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
@ -54,9 +70,11 @@ let PostDropdownBtn = ({
|
||||||
richText: RichTextAPI
|
richText: RichTextAPI
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showAppealLabelItem?: boolean
|
showAppealLabelItem?: boolean
|
||||||
|
hitSlop?: PressableProps['hitSlop']
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const alf = useAlf()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
@ -151,173 +169,189 @@ let PostDropdownBtn = ({
|
||||||
hidePost({uri: postUri})
|
hidePost({uri: postUri})
|
||||||
}, [postUri, hidePost])
|
}, [postUri, hidePost])
|
||||||
|
|
||||||
const dropdownItems: NativeDropdownItem[] = [
|
return (
|
||||||
{
|
<EventStopper onKeyDown={false}>
|
||||||
label: _(msg`Translate`),
|
<Menu.Root>
|
||||||
onPress() {
|
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||||
onOpenTranslate()
|
{({props, state}) => {
|
||||||
},
|
const styles = [
|
||||||
testID: 'postDropdownTranslateBtn',
|
style,
|
||||||
icon: {
|
a.rounded_full,
|
||||||
ios: {
|
(state.hovered || state.focused || state.pressed) && [
|
||||||
name: 'character.book.closed',
|
web({outline: 0}),
|
||||||
},
|
alf.atoms.bg_contrast_25,
|
||||||
android: 'ic_menu_sort_alphabetically',
|
],
|
||||||
web: 'language',
|
]
|
||||||
},
|
return isWeb ? (
|
||||||
},
|
<View {...props} testID={testID} style={styles}>
|
||||||
{
|
<FontAwesomeIcon
|
||||||
label: _(msg`Copy post text`),
|
icon="ellipsis"
|
||||||
onPress() {
|
size={20}
|
||||||
onCopyPostText()
|
color={defaultCtrlColor}
|
||||||
},
|
style={{pointerEvents: 'none'}}
|
||||||
testID: 'postDropdownCopyTextBtn',
|
/>
|
||||||
icon: {
|
</View>
|
||||||
ios: {
|
) : (
|
||||||
name: 'doc.on.doc',
|
<Pressable
|
||||||
},
|
{...props}
|
||||||
android: 'ic_menu_edit',
|
hitSlop={hitSlop}
|
||||||
web: ['far', 'paste'],
|
testID={testID}
|
||||||
},
|
style={styles}>
|
||||||
},
|
<FontAwesomeIcon
|
||||||
{
|
icon="ellipsis"
|
||||||
label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`),
|
size={20}
|
||||||
onPress() {
|
color={defaultCtrlColor}
|
||||||
|
style={{pointerEvents: 'none'}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Menu.Trigger>
|
||||||
|
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="postDropdownTranslateBtn"
|
||||||
|
label={_(msg`Translate`)}
|
||||||
|
onPress={onOpenTranslate}>
|
||||||
|
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Translate} position="right" />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
testID="postDropdownCopyTextBtn"
|
||||||
|
label={_(msg`Copy post text`)}
|
||||||
|
onPress={onCopyPostText}>
|
||||||
|
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||||
|
</Menu.Item>
|
||||||
|
|
||||||
|
<Menu.Item
|
||||||
|
testID="postDropdownShareBtn"
|
||||||
|
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||||
|
onPress={() => {
|
||||||
const url = toShareUrl(href)
|
const url = toShareUrl(href)
|
||||||
shareUrl(url)
|
shareUrl(url)
|
||||||
},
|
}}>
|
||||||
testID: 'postDropdownShareBtn',
|
<Menu.ItemText>
|
||||||
icon: {
|
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||||
ios: {
|
</Menu.ItemText>
|
||||||
name: 'square.and.arrow.up',
|
<Menu.ItemIcon icon={Share} position="right" />
|
||||||
},
|
</Menu.Item>
|
||||||
android: 'ic_menu_share',
|
</Menu.Group>
|
||||||
web: 'share',
|
|
||||||
},
|
{hasSession && (
|
||||||
},
|
<>
|
||||||
hasSession && {
|
<Menu.Divider />
|
||||||
label: 'separator',
|
|
||||||
},
|
<Menu.Group>
|
||||||
hasSession && {
|
<Menu.Item
|
||||||
label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
|
testID="postDropdownMuteThreadBtn"
|
||||||
onPress() {
|
label={
|
||||||
onToggleThreadMute()
|
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||||
},
|
}
|
||||||
testID: 'postDropdownMuteThreadBtn',
|
onPress={onToggleThreadMute}>
|
||||||
icon: {
|
<Menu.ItemText>
|
||||||
ios: {
|
{isThreadMuted
|
||||||
name: 'speaker.slash',
|
? _(msg`Unmute thread`)
|
||||||
},
|
: _(msg`Mute thread`)}
|
||||||
android: 'ic_lock_silent_mode',
|
</Menu.ItemText>
|
||||||
web: 'comment-slash',
|
<Menu.ItemIcon
|
||||||
},
|
icon={isThreadMuted ? Unmute : Mute}
|
||||||
},
|
position="right"
|
||||||
hasSession && {
|
/>
|
||||||
label: _(msg`Mute words & tags`),
|
</Menu.Item>
|
||||||
onPress() {
|
|
||||||
mutedWordsDialogControl.open()
|
<Menu.Item
|
||||||
},
|
testID="postDropdownMuteWordsBtn"
|
||||||
testID: 'postDropdownMuteWordsBtn',
|
label={_(msg`Mute words & tags`)}
|
||||||
icon: {
|
onPress={() => mutedWordsDialogControl.open()}>
|
||||||
ios: {
|
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
|
||||||
name: 'speaker.slash',
|
<Menu.ItemIcon icon={Filter} position="right" />
|
||||||
},
|
</Menu.Item>
|
||||||
android: 'ic_lock_silent_mode',
|
|
||||||
web: 'filter',
|
{!isAuthor && !isPostHidden && (
|
||||||
},
|
<Menu.Item
|
||||||
},
|
testID="postDropdownHideBtn"
|
||||||
hasSession &&
|
label={_(msg`Hide post`)}
|
||||||
!isAuthor &&
|
onPress={() => {
|
||||||
!isPostHidden && {
|
|
||||||
label: _(msg`Hide post`),
|
|
||||||
onPress() {
|
|
||||||
openModal({
|
openModal({
|
||||||
name: 'confirm',
|
name: 'confirm',
|
||||||
title: _(msg`Hide this post?`),
|
title: _(msg`Hide this post?`),
|
||||||
message: _(msg`This will hide this post from your feeds.`),
|
message: _(
|
||||||
|
msg`This will hide this post from your feeds.`,
|
||||||
|
),
|
||||||
onPressConfirm: onHidePost,
|
onPressConfirm: onHidePost,
|
||||||
})
|
})
|
||||||
},
|
}}>
|
||||||
testID: 'postDropdownHideBtn',
|
<Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
|
||||||
icon: {
|
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||||
ios: {
|
</Menu.Item>
|
||||||
name: 'eye.slash',
|
)}
|
||||||
},
|
</Menu.Group>
|
||||||
android: 'ic_menu_delete',
|
</>
|
||||||
web: ['far', 'eye-slash'],
|
)}
|
||||||
},
|
|
||||||
},
|
<Menu.Divider />
|
||||||
{
|
|
||||||
label: 'separator',
|
<Menu.Group>
|
||||||
},
|
{!isAuthor && (
|
||||||
!isAuthor &&
|
<Menu.Item
|
||||||
hasSession && {
|
testID="postDropdownReportBtn"
|
||||||
label: _(msg`Report post`),
|
label={_(msg`Report post`)}
|
||||||
onPress() {
|
onPress={() => {
|
||||||
openModal({
|
openModal({
|
||||||
name: 'report',
|
name: 'report',
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
cid: postCid,
|
cid: postCid,
|
||||||
})
|
})
|
||||||
},
|
}}>
|
||||||
testID: 'postDropdownReportBtn',
|
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||||
icon: {
|
<Menu.ItemIcon icon={Warning} position="right" />
|
||||||
ios: {
|
</Menu.Item>
|
||||||
name: 'exclamationmark.triangle',
|
)}
|
||||||
},
|
|
||||||
android: 'ic_menu_report_image',
|
{isAuthor && (
|
||||||
web: 'circle-exclamation',
|
<Menu.Item
|
||||||
},
|
testID="postDropdownDeleteBtn"
|
||||||
},
|
label={_(msg`Delete post`)}
|
||||||
isAuthor && {
|
onPress={() => {
|
||||||
label: _(msg`Delete post`),
|
|
||||||
onPress() {
|
|
||||||
openModal({
|
openModal({
|
||||||
name: 'confirm',
|
name: 'confirm',
|
||||||
title: _(msg`Delete this post?`),
|
title: _(msg`Delete this post?`),
|
||||||
message: _(msg`Are you sure? This cannot be undone.`),
|
message: _(msg`Are you sure? This cannot be undone.`),
|
||||||
onPressConfirm: onDeletePost,
|
onPressConfirm: onDeletePost,
|
||||||
})
|
})
|
||||||
},
|
}}>
|
||||||
testID: 'postDropdownDeleteBtn',
|
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||||
icon: {
|
<Menu.ItemIcon icon={Trash} position="right" />
|
||||||
ios: {
|
</Menu.Item>
|
||||||
name: 'trash',
|
)}
|
||||||
},
|
|
||||||
android: 'ic_menu_delete',
|
|
||||||
web: ['far', 'trash-can'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
showAppealLabelItem && {
|
|
||||||
label: 'separator',
|
|
||||||
},
|
|
||||||
showAppealLabelItem && {
|
|
||||||
label: _(msg`Appeal content warning`),
|
|
||||||
onPress() {
|
|
||||||
openModal({name: 'appeal-label', uri: postUri, cid: postCid})
|
|
||||||
},
|
|
||||||
testID: 'postDropdownAppealBtn',
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'exclamationmark.triangle',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_report_image',
|
|
||||||
web: 'circle-exclamation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
].filter(Boolean) as NativeDropdownItem[]
|
|
||||||
|
|
||||||
return (
|
{showAppealLabelItem && (
|
||||||
<EventStopper>
|
<>
|
||||||
<NativeDropdown
|
<Menu.Divider />
|
||||||
testID={testID}
|
|
||||||
items={dropdownItems}
|
<Menu.Item
|
||||||
accessibilityLabel={_(msg`More post options`)}
|
testID="postDropdownAppealBtn"
|
||||||
accessibilityHint="">
|
label={_(msg`Appeal content warning`)}
|
||||||
<View style={style}>
|
onPress={() => {
|
||||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
openModal({
|
||||||
</View>
|
name: 'appeal-label',
|
||||||
</NativeDropdown>
|
uri: postUri,
|
||||||
|
cid: postCid,
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{_(msg`Appeal content warning`)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={CircleInfo} position="right" />
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Group>
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
</EventStopper>
|
</EventStopper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,9 +212,7 @@ let PostCtrls = ({
|
||||||
style={[styles.btn]}
|
style={[styles.btn]}
|
||||||
onPress={onShare}
|
onPress={onShare}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`${
|
accessibilityLabel={`${_(msg`Share`)}`}
|
||||||
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
|
|
||||||
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
|
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||||
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
|
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
|
||||||
|
@ -231,6 +229,7 @@ let PostCtrls = ({
|
||||||
richText={richText}
|
richText={richText}
|
||||||
showAppealLabelItem={showAppealLabelItem}
|
showAppealLabelItem={showAppealLabelItem}
|
||||||
style={styles.btnPad}
|
style={styles.btnPad}
|
||||||
|
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -30,7 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util'
|
||||||
import * as notifications from 'lib/notifications/notifications'
|
import * as notifications from 'lib/notifications/notifications'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
import {useDialogStateContext} from '#/state/dialogs'
|
import {useDialogStateContext} from 'state/dialogs'
|
||||||
|
import Animated from 'react-native-reanimated'
|
||||||
|
|
||||||
function ShellInner() {
|
function ShellInner() {
|
||||||
const isDrawerOpen = useIsDrawerOpen()
|
const isDrawerOpen = useIsDrawerOpen()
|
||||||
|
@ -54,9 +55,9 @@ function ShellInner() {
|
||||||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||||
|
const {importantForAccessibility} = useDialogStateContext()
|
||||||
// start undefined
|
// start undefined
|
||||||
const currentAccountDid = React.useRef<string | undefined>(undefined)
|
const currentAccountDid = React.useRef<string | undefined>(undefined)
|
||||||
const {openDialogs} = useDialogStateContext()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let listener = {remove() {}}
|
let listener = {remove() {}}
|
||||||
|
@ -80,19 +81,9 @@ function ShellInner() {
|
||||||
}
|
}
|
||||||
}, [currentAccount])
|
}, [currentAccount])
|
||||||
|
|
||||||
/**
|
|
||||||
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
|
||||||
* applies to the parent of all non-modal views, and prevents TalkBack from
|
|
||||||
* navigating within content beneath an open dialog.
|
|
||||||
*
|
|
||||||
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
|
||||||
*/
|
|
||||||
const importantForAccessibility =
|
|
||||||
openDialogs.length > 0 ? 'no-hide-descendants' : undefined
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View
|
<Animated.View
|
||||||
style={containerPadding}
|
style={containerPadding}
|
||||||
importantForAccessibility={importantForAccessibility}>
|
importantForAccessibility={importantForAccessibility}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
@ -106,7 +97,7 @@ function ShellInner() {
|
||||||
<TabsNavigator />
|
<TabsNavigator />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</View>
|
</Animated.View>
|
||||||
<Composer winHeight={winDim.height} />
|
<Composer winHeight={winDim.height} />
|
||||||
<ModalsContainer />
|
<ModalsContainer />
|
||||||
<MutedWordsDialog />
|
<MutedWordsDialog />
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
height: calc(100% + env(safe-area-inset-top));
|
height: calc(100% + env(safe-area-inset-top));
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
html, body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||||
button, input, textarea {
|
button, input, textarea {
|
||||||
|
|
Loading…
Reference in New Issue