Merge remote-tracking branch 'origin' into xrpc-rework
This commit is contained in:
+45
-39
@@ -23,10 +23,12 @@ import {
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -95,7 +96,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -104,42 +108,44 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
<ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</ActiveVideoProvider>
|
||||
</Splash>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
+42
-36
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -77,7 +78,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -89,39 +93,41 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
+14
-9
@@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
|
||||
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
|
||||
import {NotFoundScreen} from './view/screens/NotFound'
|
||||
import {NotificationsScreen} from './view/screens/Notifications'
|
||||
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
|
||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||
import {PostThreadScreen} from './view/screens/PostThread'
|
||||
@@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesSettingsScreen}
|
||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationsSettings"
|
||||
getComponent={() => NotificationsSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Feeds"
|
||||
getComponent={() => FeedsScreen}
|
||||
@@ -771,15 +777,14 @@ function logModuleInitTime() {
|
||||
})
|
||||
|
||||
if (isWeb) {
|
||||
Referrer.getReferrerInfoAsync().then(info => {
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: window.location.href,
|
||||
referrer: info?.referrer,
|
||||
hostname: info?.hostname,
|
||||
})
|
||||
}
|
||||
})
|
||||
const referrerInfo = Referrer.getReferrerInfo()
|
||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: window.location.href,
|
||||
referrer: referrerInfo?.referrer,
|
||||
hostname: referrerInfo?.hostname,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
|
||||
+8
-6
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const atoms = {
|
||||
/*
|
||||
@@ -145,6 +144,9 @@ export const atoms = {
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flex_shrink_0: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
justify_start: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
@@ -281,19 +283,19 @@ export const atoms = {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: hairlineWidth,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
+26
-26
@@ -261,23 +261,23 @@ export function createThemes({
|
||||
contrast_950: `hsl(${hues.primary}, 20%, ${dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${hues.primary}, 20%, ${dimScale[13]}%)`,
|
||||
|
||||
primary_25: `hsl(${hues.primary}, 99%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 99%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 99%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 99%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 99%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 99%, ${dimScale[6]}%)`,
|
||||
primary_25: `hsl(${hues.primary}, 50%, ${dimScale[1]}%)`,
|
||||
primary_50: `hsl(${hues.primary}, 60%, ${dimScale[2]}%)`,
|
||||
primary_100: `hsl(${hues.primary}, 70%, ${dimScale[3]}%)`,
|
||||
primary_200: `hsl(${hues.primary}, 82%, ${dimScale[4]}%)`,
|
||||
primary_300: `hsl(${hues.primary}, 90%, ${dimScale[5]}%)`,
|
||||
primary_400: `hsl(${hues.primary}, 95%, ${dimScale[6]}%)`,
|
||||
primary_500: `hsl(${hues.primary}, 99%, ${dimScale[7]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 95%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 90%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 82%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 70%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 60%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 50%, ${dimScale[13]}%)`,
|
||||
primary_600: `hsl(${hues.primary}, 99%, ${dimScale[8]}%)`,
|
||||
primary_700: `hsl(${hues.primary}, 99%, ${dimScale[9]}%)`,
|
||||
primary_800: `hsl(${hues.primary}, 99%, ${dimScale[10]}%)`,
|
||||
primary_900: `hsl(${hues.primary}, 99%, ${dimScale[11]}%)`,
|
||||
primary_950: `hsl(${hues.primary}, 99%, ${dimScale[12]}%)`,
|
||||
primary_975: `hsl(${hues.primary}, 99%, ${dimScale[13]}%)`,
|
||||
|
||||
positive_25: `hsl(${hues.positive}, 82%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 82%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 82%, ${dimScale[3]}%)`,
|
||||
positive_25: `hsl(${hues.positive}, 50%, ${dimScale[1]}%)`,
|
||||
positive_50: `hsl(${hues.positive}, 60%, ${dimScale[2]}%)`,
|
||||
positive_100: `hsl(${hues.positive}, 70%, ${dimScale[3]}%)`,
|
||||
positive_200: `hsl(${hues.positive}, 82%, ${dimScale[4]}%)`,
|
||||
positive_300: `hsl(${hues.positive}, 82%, ${dimScale[5]}%)`,
|
||||
positive_400: `hsl(${hues.positive}, 82%, ${dimScale[6]}%)`,
|
||||
@@ -285,23 +285,23 @@ export function createThemes({
|
||||
positive_600: `hsl(${hues.positive}, 82%, ${dimScale[8]}%)`,
|
||||
positive_700: `hsl(${hues.positive}, 82%, ${dimScale[9]}%)`,
|
||||
positive_800: `hsl(${hues.positive}, 82%, ${dimScale[10]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 70%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 60%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 50%, ${dimScale[13]}%)`,
|
||||
positive_900: `hsl(${hues.positive}, 82%, ${dimScale[11]}%)`,
|
||||
positive_950: `hsl(${hues.positive}, 82%, ${dimScale[12]}%)`,
|
||||
positive_975: `hsl(${hues.positive}, 82%, ${dimScale[13]}%)`,
|
||||
|
||||
negative_25: `hsl(${hues.negative}, 91%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 91%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 91%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 91%, ${dimScale[4]}%)`,
|
||||
negative_25: `hsl(${hues.negative}, 70%, ${dimScale[1]}%)`,
|
||||
negative_50: `hsl(${hues.negative}, 80%, ${dimScale[2]}%)`,
|
||||
negative_100: `hsl(${hues.negative}, 84%, ${dimScale[3]}%)`,
|
||||
negative_200: `hsl(${hues.negative}, 88%, ${dimScale[4]}%)`,
|
||||
negative_300: `hsl(${hues.negative}, 91%, ${dimScale[5]}%)`,
|
||||
negative_400: `hsl(${hues.negative}, 91%, ${dimScale[6]}%)`,
|
||||
negative_500: `hsl(${hues.negative}, 91%, ${dimScale[7]}%)`,
|
||||
negative_600: `hsl(${hues.negative}, 91%, ${dimScale[8]}%)`,
|
||||
negative_700: `hsl(${hues.negative}, 91%, ${dimScale[9]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 88%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 84%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 80%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 70%, ${dimScale[13]}%)`,
|
||||
negative_800: `hsl(${hues.negative}, 91%, ${dimScale[10]}%)`,
|
||||
negative_900: `hsl(${hues.negative}, 91%, ${dimScale[11]}%)`,
|
||||
negative_950: `hsl(${hues.negative}, 91%, ${dimScale[12]}%)`,
|
||||
negative_975: `hsl(${hues.negative}, 91%, ${dimScale[13]}%)`,
|
||||
} as const
|
||||
|
||||
const light: Theme = {
|
||||
|
||||
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
testID?: string
|
||||
/**
|
||||
* For a11y, try to make this descriptive and clear
|
||||
*/
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view} = props
|
||||
return (
|
||||
<Link label={view.displayName} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -58,7 +58,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -70,7 +70,11 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} style={[a.flex_col]} {...props}>
|
||||
<InternalLink
|
||||
label={view.displayName}
|
||||
to={href}
|
||||
style={[a.flex_col]}
|
||||
{...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
@@ -264,9 +268,9 @@ function SaveButtonInner({
|
||||
])
|
||||
}
|
||||
Toast.show(_(msg`Feeds updated!`))
|
||||
} catch (e: any) {
|
||||
logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`))
|
||||
} catch (err: any) {
|
||||
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||
}
|
||||
},
|
||||
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
|
||||
|
||||
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
|
||||
{profiles.slice(0, maxLength).map(profile => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
did={profile.handle}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('feed:interstitial:profileCard:press', {})
|
||||
}}
|
||||
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
|
||||
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
|
||||
> & {
|
||||
testID?: string
|
||||
|
||||
/**
|
||||
* Label for a11y. Defaults to the href.
|
||||
*/
|
||||
label?: string
|
||||
|
||||
/**
|
||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||
*/
|
||||
@@ -197,7 +192,7 @@ export function useLink({
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
|
||||
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||
Omit<ButtonProps, 'onPress' | 'disabled'>
|
||||
|
||||
/**
|
||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||
@@ -224,7 +219,6 @@ export function Link({
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={href}
|
||||
{...rest}
|
||||
style={[a.justify_start, flatten(rest.style)]}
|
||||
role="link"
|
||||
@@ -249,7 +243,8 @@ export function Link({
|
||||
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
|
||||
>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -291,7 +286,7 @@ export function InlineLinkText({
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label || href}
|
||||
accessibilityLabel={label}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
|
||||
@@ -44,7 +44,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view, showPinButton} = props
|
||||
return (
|
||||
<Link label={view.name} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -67,7 +67,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -79,7 +79,7 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} {...props}>
|
||||
<InternalLink label={view.name} to={href} {...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
|
||||
@@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
|
||||
return (
|
||||
<Error
|
||||
title={errorTitle ?? _(msg`Oops!`)}
|
||||
message={errorMessage ?? _(`Something went wrong!`)}
|
||||
message={errorMessage ?? _(msg`Something went wrong!`)}
|
||||
onRetry={onRetry}
|
||||
onGoBack={onGoBack}
|
||||
sideBorders={sideBorders}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function Default({
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
}) {
|
||||
return (
|
||||
<Link did={profile.did}>
|
||||
<Link profile={profile}>
|
||||
<Card
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
@@ -96,16 +96,24 @@ export function Header({
|
||||
}
|
||||
|
||||
export function Link({
|
||||
did,
|
||||
profile,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
}: {did: string} & Omit<LinkProps, 'to'>) {
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
} & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<InternalLink
|
||||
label={_(
|
||||
msg`View ${
|
||||
profile.displayName || sanitizeHandle(profile.handle)
|
||||
}'s profile`,
|
||||
)}
|
||||
to={{
|
||||
screen: 'Profile',
|
||||
params: {name: did},
|
||||
params: {name: profile.did},
|
||||
}}
|
||||
style={[a.flex_col, style]}
|
||||
{...rest}>
|
||||
@@ -306,9 +314,9 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,9 +334,9 @@ export function FollowButtonInner({
|
||||
)}`,
|
||||
),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,10 @@ export function QrCodeDialog({
|
||||
try {
|
||||
await createAssetAsync(`file://${uri}`)
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
||||
Toast.show(
|
||||
_(msg`An error occurred while saving the QR code!`),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ function ShareDialogInner({
|
||||
if (!res) {
|
||||
Toast.show(
|
||||
_(msg`You must grant access to your photo library to save the image.`),
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -75,7 +76,7 @@ function ShareDialogInner({
|
||||
Toast.show(_(msg`Image saved to your camera roll!`))
|
||||
control.close()
|
||||
} catch (e: unknown) {
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`))
|
||||
Toast.show(_(msg`An error occurred while saving the QR code!`), 'xmark')
|
||||
logger.error('Failed to save QR code', {error: e})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
|
||||
_(
|
||||
msg`There was an issue. Please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error('Failed to edit threadgate', {message: err})
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ function DialogContent({
|
||||
|
||||
const onPressAudience = (setting: ThreadgateSetting) => {
|
||||
// remove nobody
|
||||
let newSelected = draft.filter(v => v.type !== 'nobody')
|
||||
let newSelected: ThreadgateSetting[] = draft.filter(
|
||||
v => v.type !== 'nobody',
|
||||
)
|
||||
// toggle
|
||||
const i = newSelected.findIndex(v => isEqual(v, setting))
|
||||
if (i === -1) {
|
||||
|
||||
@@ -42,6 +42,7 @@ export function BlockedByListDialog({
|
||||
<React.Fragment key={block.source.list.uri}>
|
||||
{i === 0 ? null : ', '}
|
||||
<InlineLinkText
|
||||
label={block.source.list.name}
|
||||
to={listUriToHref(block.source.list.uri)}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{block.source.list.name}
|
||||
|
||||
@@ -85,7 +85,7 @@ let ConvoMenu = ({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not mute chat`))
|
||||
Toast.show(_(msg`Could not mute chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export function LeaveConvoPrompt({
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Could not leave chat`))
|
||||
Toast.show(_(msg`Could not leave chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export let MessageMenu = ({
|
||||
)
|
||||
|
||||
Clipboard.setStringAsync(str)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}, [_, message.text, message.facets])
|
||||
|
||||
const onPressTranslateMessage = React.useCallback(() => {
|
||||
|
||||
@@ -140,6 +140,7 @@ function HeaderReady({
|
||||
userBlock?: ModerationCause
|
||||
}
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
@@ -156,6 +157,7 @@ function HeaderReady({
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Link
|
||||
label={_(msg`View ${displayName}'s profile`)}
|
||||
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
|
||||
to={makeProfileLink(profile)}>
|
||||
<View style={[a.pt_2xs]}>
|
||||
|
||||
@@ -57,7 +57,7 @@ function DialogInner({
|
||||
const [initialized, setInitialzed] = React.useState(false)
|
||||
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to update settings`))
|
||||
Toast.show(_(msg`Failed to update settings`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
@@ -31,7 +32,8 @@ export function NewChat({
|
||||
logEvent('chat:open', {logContext: 'NewChatDialog'})
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
logger.error('Failed to create chat', {safeMessage: error})
|
||||
Toast.show(_(msg`An issue occurred starting the chat`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -43,7 +44,11 @@ function SendViaChatDialogInner({
|
||||
logEvent('chat:open', {logContext: 'SendViaChatDialog'})
|
||||
},
|
||||
onError: error => {
|
||||
Toast.show(error.message)
|
||||
logger.error('Failed to share post to chat', {message: error})
|
||||
Toast.show(
|
||||
_(msg`An issue occurred while trying to open the chat`),
|
||||
'xmark',
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View, ViewStyle} from 'react-native'
|
||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {
|
||||
atoms as a,
|
||||
@@ -244,6 +246,7 @@ export function LabelText({
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
{
|
||||
userSelect: 'none',
|
||||
color: disabled
|
||||
@@ -251,7 +254,7 @@ export function LabelText({
|
||||
: t.atoms.text_contrast_high.color,
|
||||
},
|
||||
native({
|
||||
paddingTop: 3,
|
||||
paddingTop: 2,
|
||||
}),
|
||||
flatten(style),
|
||||
]}>
|
||||
@@ -344,10 +347,10 @@ export function Checkbox() {
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.border,
|
||||
a.rounded_xs,
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 20,
|
||||
},
|
||||
@@ -375,34 +378,35 @@ export function Switch() {
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 30,
|
||||
width: 32,
|
||||
padding: 2,
|
||||
},
|
||||
baseStyles,
|
||||
hovered ? baseHoverStyles : {},
|
||||
]}>
|
||||
<View
|
||||
<Animated.View
|
||||
layout={LinearTransition.duration(100)}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
{
|
||||
height: 12,
|
||||
width: 12,
|
||||
top: 3,
|
||||
left: 3,
|
||||
backgroundColor: t.palette.contrast_400,
|
||||
height: 14,
|
||||
width: 14,
|
||||
},
|
||||
selected
|
||||
? {
|
||||
backgroundColor: t.palette.primary_500,
|
||||
left: 13,
|
||||
alignSelf: 'flex-end',
|
||||
}
|
||||
: {},
|
||||
: {
|
||||
backgroundColor: t.palette.contrast_400,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
indicatorStyles,
|
||||
]}
|
||||
/>
|
||||
@@ -428,10 +432,10 @@ export function Radio() {
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.border,
|
||||
a.rounded_full,
|
||||
t.atoms.border_contrast_high,
|
||||
{
|
||||
borderWidth: 1,
|
||||
height: 20,
|
||||
width: 20,
|
||||
},
|
||||
@@ -456,3 +460,5 @@ export function Radio() {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const Platform = isNative ? Switch : Checkbox
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useFollowMethods({
|
||||
} catch (e: any) {
|
||||
logger.error(`useFollowMethods: failed to follow`, {message: String(e)})
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -47,7 +47,7 @@ export function useFollowMethods({
|
||||
message: String(e),
|
||||
})
|
||||
if (e?.name !== 'AbortError') {
|
||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||
Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Play_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z',
|
||||
})
|
||||
|
||||
export const Play_Filled_Corner2_Rounded = createSinglePathSVG({
|
||||
path: 'M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z',
|
||||
})
|
||||
@@ -174,7 +174,7 @@ export function LabelerLabelPreference({
|
||||
disabled?: boolean
|
||||
labelerDid?: string
|
||||
}) {
|
||||
const {i18n} = useLingui()
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
|
||||
@@ -243,7 +243,10 @@ export function LabelerLabelPreference({
|
||||
) : isGlobalLabel ? (
|
||||
<Trans>
|
||||
Configured in{' '}
|
||||
<InlineLinkText to="/moderation" style={a.text_sm}>
|
||||
<InlineLinkText
|
||||
label={_(msg`moderation settings`)}
|
||||
to="/moderation"
|
||||
style={a.text_sm}>
|
||||
moderation settings
|
||||
</InlineLinkText>
|
||||
.
|
||||
|
||||
@@ -128,6 +128,9 @@ function Label({
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {labeler, strings} = useLabelInfo(label)
|
||||
const sourceName = labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -169,13 +172,12 @@ function Label({
|
||||
<Trans>
|
||||
Source:{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}>
|
||||
{labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src}
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
)}
|
||||
@@ -203,6 +205,9 @@ function AppealForm({
|
||||
const isAccountReport = 'did' in subject
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
const sourceName = labeler
|
||||
? sanitizeHandle(labeler.creator.handle, '@')
|
||||
: label.src
|
||||
|
||||
const {mutate, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -241,7 +246,7 @@ function AppealForm({
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to submit label appeal', {message: err})
|
||||
Toast.show(_(msg`Failed to submit appeal, please try again.`))
|
||||
Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close()
|
||||
@@ -260,12 +265,13 @@ function AppealForm({
|
||||
<Trans>
|
||||
This appeal will be sent to{' '}
|
||||
<InlineLinkText
|
||||
label={sourceName}
|
||||
to={makeProfileLink(
|
||||
labeler ? labeler.creator : {did: label.src, handle: ''},
|
||||
)}
|
||||
onPress={() => control.close()}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
|
||||
{sourceName}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
|
||||
@@ -54,7 +54,10 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
<InlineLinkText
|
||||
label={list.name}
|
||||
to={listUriToHref(list.uri)}
|
||||
style={[a.text_sm]}>
|
||||
{list.name}
|
||||
</InlineLinkText>{' '}
|
||||
list which you have blocked.
|
||||
@@ -83,7 +86,10 @@ function ModerationDetailsDialogInner({
|
||||
description = (
|
||||
<Trans>
|
||||
This user is included in the{' '}
|
||||
<InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
|
||||
<InlineLinkText
|
||||
label={list.name}
|
||||
to={listUriToHref(list.uri)}
|
||||
style={[a.text_sm]}>
|
||||
{list.name}
|
||||
</InlineLinkText>{' '}
|
||||
list which you have muted.
|
||||
@@ -127,10 +133,11 @@ function ModerationDetailsDialogInner({
|
||||
<Trans>
|
||||
This label was applied by{' '}
|
||||
<InlineLinkText
|
||||
label={desc.source || _(msg`an unknown labeler`)}
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source}
|
||||
{desc.source || _(msg`an unknown labeler`)}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
|
||||
+70
-61
@@ -14,29 +14,33 @@ export type FeedTunerFn = (
|
||||
slices: FeedViewPostsSlice[],
|
||||
) => FeedViewPostsSlice[]
|
||||
|
||||
type FeedSliceItem = {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
reply?: AppBskyFeedDefs.ReplyRef
|
||||
}
|
||||
|
||||
function toSliceItem(feedViewPost: FeedViewPost): FeedSliceItem {
|
||||
return {
|
||||
post: feedViewPost.post,
|
||||
reply: feedViewPost.reply,
|
||||
}
|
||||
}
|
||||
|
||||
export class FeedViewPostsSlice {
|
||||
_reactKey: string
|
||||
isFlattenedReply = false
|
||||
_feedPost: FeedViewPost
|
||||
items: FeedSliceItem[]
|
||||
|
||||
constructor(public items: FeedViewPost[]) {
|
||||
const item = items[0]
|
||||
this._reactKey = `slice-${item.post.uri}-${
|
||||
item.reason?.indexedAt || item.post.indexedAt
|
||||
constructor(feedPost: FeedViewPost) {
|
||||
this._feedPost = feedPost
|
||||
this._reactKey = `slice-${feedPost.post.uri}-${
|
||||
feedPost.reason?.indexedAt || feedPost.post.indexedAt
|
||||
}`
|
||||
this.items = [toSliceItem(feedPost)]
|
||||
}
|
||||
|
||||
get uri() {
|
||||
if (this.isFlattenedReply) {
|
||||
return this.items[1].post.uri
|
||||
}
|
||||
return this.items[0].post.uri
|
||||
}
|
||||
|
||||
get ts() {
|
||||
if (this.items[0].reason?.indexedAt) {
|
||||
return this.items[0].reason.indexedAt as string
|
||||
}
|
||||
return this.items[0].post.indexedAt
|
||||
return this._feedPost.post.uri
|
||||
}
|
||||
|
||||
get isThread() {
|
||||
@@ -48,31 +52,42 @@ export class FeedViewPostsSlice {
|
||||
)
|
||||
}
|
||||
|
||||
get isFullThread() {
|
||||
return this.isThread && !this.items[0].reply
|
||||
}
|
||||
|
||||
get rootItem() {
|
||||
if (this.isFlattenedReply) {
|
||||
return this.items[1]
|
||||
}
|
||||
return this.items[0]
|
||||
get isQuotePost() {
|
||||
const embed = this._feedPost.post.embed
|
||||
return (
|
||||
AppBskyEmbedRecord.isView(embed) ||
|
||||
AppBskyEmbedRecordWithMedia.isView(embed)
|
||||
)
|
||||
}
|
||||
|
||||
get isReply() {
|
||||
return (
|
||||
AppBskyFeedPost.isRecord(this.rootItem.post.record) &&
|
||||
!!this.rootItem.post.record.reply
|
||||
AppBskyFeedPost.isRecord(this._feedPost.post.record) &&
|
||||
!!this._feedPost.post.record.reply
|
||||
)
|
||||
}
|
||||
|
||||
get source(): ReasonFeedSource | undefined {
|
||||
return this.items.find(item => '__source' in item && !!item.__source)
|
||||
?.__source as ReasonFeedSource
|
||||
get reason() {
|
||||
return '__source' in this._feedPost
|
||||
? (this._feedPost.__source as ReasonFeedSource)
|
||||
: this._feedPost.reason
|
||||
}
|
||||
|
||||
get feedContext() {
|
||||
return this.items.find(item => item.feedContext)?.feedContext
|
||||
return this._feedPost.feedContext
|
||||
}
|
||||
|
||||
get isRepost() {
|
||||
const reason = this._feedPost.reason
|
||||
return AppBskyFeedDefs.isReasonRepost(reason)
|
||||
}
|
||||
|
||||
get includesThreadRoot() {
|
||||
return !this.items[0].reply
|
||||
}
|
||||
|
||||
get likeCount() {
|
||||
return this._feedPost.post.likeCount ?? 0
|
||||
}
|
||||
|
||||
containsUri(uri: string) {
|
||||
@@ -97,24 +112,24 @@ export class FeedViewPostsSlice {
|
||||
if (this.items[0].reply) {
|
||||
const reply = this.items[0].reply
|
||||
if (AppBskyFeedDefs.isPostView(reply.parent)) {
|
||||
this.isFlattenedReply = true
|
||||
this.items.splice(0, 0, {post: reply.parent})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isFollowingAllAuthors(userDid: string) {
|
||||
const item = this.rootItem
|
||||
if (item.post.author.did === userDid) {
|
||||
const feedPost = this._feedPost
|
||||
if (feedPost.post.author.did === userDid) {
|
||||
return true
|
||||
}
|
||||
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
|
||||
const parent = item.reply?.parent
|
||||
if (AppBskyFeedDefs.isPostView(feedPost.reply?.parent)) {
|
||||
const parent = feedPost.reply?.parent
|
||||
if (parent?.author.did === userDid) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
parent?.author.viewer?.following && item.post.author.viewer?.following
|
||||
parent?.author.viewer?.following &&
|
||||
feedPost.post.author.viewer?.following
|
||||
)
|
||||
}
|
||||
return false
|
||||
@@ -127,7 +142,7 @@ export class NoopFeedTuner {
|
||||
feed: FeedViewPost[],
|
||||
_opts?: {dryRun: boolean; maintainOrder: boolean},
|
||||
): FeedViewPostsSlice[] {
|
||||
return feed.map(item => new FeedViewPostsSlice([item]))
|
||||
return feed.map(item => new FeedViewPostsSlice(item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +180,7 @@ export class FeedTuner {
|
||||
})
|
||||
|
||||
if (maintainOrder) {
|
||||
slices = feed.map(item => new FeedViewPostsSlice([item]))
|
||||
slices = feed.map(item => new FeedViewPostsSlice(item))
|
||||
} else {
|
||||
// arrange the posts into thread slices
|
||||
for (let i = feed.length - 1; i >= 0; i--) {
|
||||
@@ -192,7 +207,7 @@ export class FeedTuner {
|
||||
}
|
||||
}
|
||||
|
||||
slices.unshift(new FeedViewPostsSlice([item]))
|
||||
slices.unshift(new FeedViewPostsSlice(item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +230,7 @@ export class FeedTuner {
|
||||
|
||||
// turn non-threads with reply parents into threads
|
||||
for (const slice of slices) {
|
||||
if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) {
|
||||
if (!slice.isThread && !slice.reason && slice.items[0].reply) {
|
||||
const reply = slice.items[0].reply
|
||||
if (
|
||||
AppBskyFeedDefs.isPostView(reply.parent) &&
|
||||
@@ -256,8 +271,7 @@ export class FeedTuner {
|
||||
|
||||
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const reason = slices[i].rootItem.reason
|
||||
if (AppBskyFeedDefs.isReasonRepost(reason)) {
|
||||
if (slices[i].isRepost) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
@@ -266,11 +280,7 @@ export class FeedTuner {
|
||||
|
||||
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const embed = slices[i].rootItem.post.embed
|
||||
if (
|
||||
AppBskyEmbedRecord.isView(embed) ||
|
||||
AppBskyEmbedRecordWithMedia.isView(embed)
|
||||
) {
|
||||
if (slices[i].isQuotePost) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
@@ -315,19 +325,18 @@ export class FeedTuner {
|
||||
// remove any replies without at least minLikes likes
|
||||
for (let i = slices.length - 1; i >= 0; i--) {
|
||||
const slice = slices[i]
|
||||
if (slice.isFullThread || !slice.isReply) {
|
||||
continue
|
||||
}
|
||||
|
||||
const item = slice.rootItem
|
||||
const isRepost = Boolean(item.reason)
|
||||
if (isRepost) {
|
||||
continue
|
||||
}
|
||||
if ((item.post.likeCount || 0) < minLikes) {
|
||||
slices.splice(i, 1)
|
||||
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
|
||||
slices.splice(i, 1)
|
||||
if (slice.isReply) {
|
||||
if (slice.isThread && slice.includesThreadRoot) {
|
||||
continue
|
||||
}
|
||||
if (slice.isRepost) {
|
||||
continue
|
||||
}
|
||||
if (slice.likeCount < minLikes) {
|
||||
slices.splice(i, 1)
|
||||
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
|
||||
slices.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return slices
|
||||
|
||||
@@ -251,7 +251,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
|
||||
dryRun: false,
|
||||
maintainOrder: true,
|
||||
})
|
||||
res.data.feed = slices.map(slice => slice.rootItem)
|
||||
res.data.feed = slices.map(slice => slice._feedPost)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ interface PostOpts {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
video?: {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
extLink?: ExternalEmbedDraft
|
||||
images?: ImageModel[]
|
||||
labels?: string[]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {timeout} from './timeout'
|
||||
|
||||
export async function until(
|
||||
export async function until<T>(
|
||||
retries: number,
|
||||
delay: number,
|
||||
cond: (v: any, err: any) => boolean,
|
||||
fn: () => Promise<any>,
|
||||
cond: (v: T, err: any) => boolean,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
while (retries > 0) {
|
||||
try {
|
||||
@@ -13,7 +13,9 @@ export async function until(
|
||||
return true
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (cond(undefined, e)) {
|
||||
// TODO: change the type signature of cond to accept undefined
|
||||
// however this breaks every existing usage of until -sfn
|
||||
if (cond(undefined as unknown as T, e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,3 +133,5 @@ export const GIF_SEARCH = (params: string) =>
|
||||
`${GIF_SERVICE}/tenor/v2/search?${params}`
|
||||
export const GIF_FEATURED = (params: string) =>
|
||||
`${GIF_SERVICE}/tenor/v2/featured?${params}`
|
||||
|
||||
export const MAX_LABELERS = 20
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
export const useDedupe = () => {
|
||||
const canDo = React.useRef(true)
|
||||
|
||||
return React.useRef((cb: () => unknown) => {
|
||||
return React.useCallback((cb: () => unknown) => {
|
||||
if (canDo.current) {
|
||||
canDo.current = false
|
||||
setTimeout(() => {
|
||||
@@ -13,5 +13,5 @@ export const useDedupe = () => {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}).current
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -18,15 +18,14 @@ export function useIntentHandler() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleIncomingURL = (url: string) => {
|
||||
Referrer.getReferrerInfoAsync().then(info => {
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: url,
|
||||
referrer: info?.referrer,
|
||||
hostname: info?.hostname,
|
||||
})
|
||||
}
|
||||
})
|
||||
const referrerInfo = Referrer.getReferrerInfo()
|
||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: url,
|
||||
referrer: referrerInfo?.referrer,
|
||||
hostname: referrerInfo?.hostname,
|
||||
})
|
||||
}
|
||||
|
||||
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
||||
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
||||
|
||||
@@ -17,14 +17,14 @@ export async function openPicker(opts?: ImagePickerOptions) {
|
||||
})
|
||||
|
||||
if (response.assets && response.assets.length > 4) {
|
||||
Toast.show('You may only select up to 4 images')
|
||||
Toast.show('You may only select up to 4 images', 'exclamation-circle')
|
||||
}
|
||||
|
||||
return (response.assets ?? [])
|
||||
.slice(0, 4)
|
||||
.filter(asset => {
|
||||
if (asset.mimeType?.startsWith('image/')) return true
|
||||
Toast.show('Only image files are supported')
|
||||
Toast.show('Only image files are supported', 'exclamation-circle')
|
||||
return false
|
||||
})
|
||||
.map(image => ({
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* TEMPORARY: THIS IS A TEMPORARY PLACEHOLDER. THAT MEANS IT IS TEMPORARY. I.E. WILL BE REMOVED. NOT TO USE IN PRODUCTION.
|
||||
* @temporary
|
||||
* PS: This is a temporary placeholder for the video types. It will be removed once the actual types are implemented.
|
||||
* Not joking, this is temporary.
|
||||
*/
|
||||
|
||||
export interface JobStatus {
|
||||
jobId: string
|
||||
did: string
|
||||
cid: string
|
||||
state: JobState
|
||||
progress?: number
|
||||
errorHuman?: string
|
||||
errorMachine?: string
|
||||
}
|
||||
|
||||
export enum JobState {
|
||||
JOB_STATE_UNSPECIFIED = 'JOB_STATE_UNSPECIFIED',
|
||||
JOB_STATE_CREATED = 'JOB_STATE_CREATED',
|
||||
JOB_STATE_ENCODING = 'JOB_STATE_ENCODING',
|
||||
JOB_STATE_ENCODED = 'JOB_STATE_ENCODED',
|
||||
JOB_STATE_UPLOADING = 'JOB_STATE_UPLOADING',
|
||||
JOB_STATE_UPLOADED = 'JOB_STATE_UPLOADED',
|
||||
JOB_STATE_CDN_PROCESSING = 'JOB_STATE_CDN_PROCESSING',
|
||||
JOB_STATE_CDN_PROCESSED = 'JOB_STATE_CDN_PROCESSED',
|
||||
JOB_STATE_FAILED = 'JOB_STATE_FAILED',
|
||||
JOB_STATE_COMPLETED = 'JOB_STATE_COMPLETED',
|
||||
}
|
||||
|
||||
export interface UploadVideoResponse {
|
||||
job_id: string
|
||||
did: string
|
||||
cid: string
|
||||
state: JobState
|
||||
}
|
||||
@@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
MessagesSettings: undefined
|
||||
NotificationsSettings: undefined
|
||||
Feeds: undefined
|
||||
Start: {name: string; rkey: string}
|
||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {
|
||||
rkey?: string
|
||||
}
|
||||
StarterPackEdit: {rkey?: string}
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
||||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
@@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
@@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesTab: undefined
|
||||
@@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {
|
||||
rkey?: string
|
||||
}
|
||||
StarterPackEdit: {rkey?: string}
|
||||
}
|
||||
|
||||
// NOTE
|
||||
|
||||
+1
-1
@@ -20,6 +20,6 @@ export async function shareUrl(url: string) {
|
||||
// React Native Share is not supported by web. Web Share API
|
||||
// has increasing but not full support, so default to clipboard
|
||||
setStringAsync(url)
|
||||
Toast.show('Copied to clipboard')
|
||||
Toast.show('Copied to clipboard', 'clipboard-check')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,4 @@ export type Gate =
|
||||
| 'suggested_follows_interstitial'
|
||||
| 'ungroup_follow_backs'
|
||||
| 'videos'
|
||||
| 'small_avi_thumb'
|
||||
|
||||
+634
-548
File diff suppressed because it is too large
Load Diff
+923
-1235
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-553
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+634
-548
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+628
-554
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
+635
-545
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ export const router = new Router({
|
||||
Search: '/search',
|
||||
Feeds: '/feeds',
|
||||
Notifications: '/notifications',
|
||||
NotificationsSettings: '/notifications/settings',
|
||||
Settings: '/settings',
|
||||
LanguageSettings: '/settings/language',
|
||||
Lists: '/lists',
|
||||
|
||||
@@ -84,7 +84,7 @@ function DialogInner() {
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to submit chat appeal', {message: err})
|
||||
Toast.show(_(msg`Failed to submit appeal, please try again.`))
|
||||
Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close()
|
||||
|
||||
@@ -67,7 +67,7 @@ export function MessageInput({
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
Toast.show(_(msg`Message is too long`))
|
||||
Toast.show(_(msg`Message is too long`), 'xmark')
|
||||
return
|
||||
}
|
||||
clearDraft()
|
||||
|
||||
@@ -46,7 +46,7 @@ export function MessageInput({
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
Toast.show(_(msg`Message is too long`))
|
||||
Toast.show(_(msg`Message is too long`), 'xmark')
|
||||
return
|
||||
}
|
||||
clearDraft()
|
||||
|
||||
@@ -106,6 +106,7 @@ function Inner() {
|
||||
title={_(msg`Something went wrong`)}
|
||||
message={_(msg`We couldn't load this conversation`)}
|
||||
onRetry={() => convoState.error.retry()}
|
||||
sideBorders={false}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
|
||||
@@ -309,7 +309,7 @@ function DesktopHeader({
|
||||
a.gap_lg,
|
||||
a.px_lg,
|
||||
a.pr_md,
|
||||
a.py_md,
|
||||
a.py_sm,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
@@ -32,7 +32,7 @@ export function MessagesSettingsScreen({}: Props) {
|
||||
|
||||
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to update settings`))
|
||||
Toast.show(_(msg`Failed to update settings`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ export function MessagesSettingsScreen({}: Props) {
|
||||
)
|
||||
|
||||
return (
|
||||
<CenteredView sideBorders style={a.h_full_vh}>
|
||||
<ScrollView stickyHeaderIndices={[0]}>
|
||||
<ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
|
||||
<View style={[a.p_lg, a.gap_md]}>
|
||||
<Text style={[a.text_lg, a.font_bold]}>
|
||||
@@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[t.atoms.text_contrast_high]}>
|
||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||
<Trans>
|
||||
You can continue ongoing conversations regardless of which setting
|
||||
you choose.
|
||||
@@ -149,6 +149,6 @@ export function MessagesSettingsScreen({}: Props) {
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -240,7 +240,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Button>
|
||||
<Divider />
|
||||
<Link testID="moderationlistsBtn" to="/moderation/modlists">
|
||||
<Link
|
||||
label={_(msg`View your moderation lists`)}
|
||||
testID="moderationlistsBtn"
|
||||
to="/moderation/modlists">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Moderation lists`)}
|
||||
@@ -252,7 +255,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link testID="mutedAccountsBtn" to="/moderation/muted-accounts">
|
||||
<Link
|
||||
label={_(msg`View your muted accounts`)}
|
||||
testID="mutedAccountsBtn"
|
||||
to="/moderation/muted-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Muted accounts`)}
|
||||
@@ -264,7 +270,10 @@ export function ModerationScreenInner({
|
||||
)}
|
||||
</Link>
|
||||
<Divider />
|
||||
<Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts">
|
||||
<Link
|
||||
label={_(msg`View your blocked accounts`)}
|
||||
testID="blockedAccountsBtn"
|
||||
to="/moderation/blocked-accounts">
|
||||
{state => (
|
||||
<SubItem
|
||||
title={_(msg`Blocked accounts`)}
|
||||
@@ -356,6 +365,7 @@ export function ModerationScreenInner({
|
||||
<Trans>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`The Bluesky web application`)}
|
||||
to=""
|
||||
onPress={evt => {
|
||||
evt.preventDefault()
|
||||
@@ -569,7 +579,9 @@ function PwiOptOut() {
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
|
||||
<InlineLinkText
|
||||
label={_(msg`Learn more about what is public on Bluesky.`)}
|
||||
to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
|
||||
<Trans>Learn more about what is public on Bluesky.</Trans>
|
||||
</InlineLinkText>
|
||||
</View>
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import {msg, Plural, plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import {MAX_LABELERS} from '#/lib/constants'
|
||||
import {isAppLabeler} from '#/lib/moderation'
|
||||
import {logger} from '#/logger'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
@@ -75,14 +77,14 @@ let ProfileHeaderLabeler = ({
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {mutateAsync: toggleSubscription, variables} =
|
||||
useLabelerSubscriptionMutation()
|
||||
const {
|
||||
mutateAsync: toggleSubscription,
|
||||
variables,
|
||||
reset,
|
||||
} = useLabelerSubscriptionMutation()
|
||||
const isSubscribed =
|
||||
variables?.subscribe ??
|
||||
preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
|
||||
const canSubscribe =
|
||||
isSubscribed ||
|
||||
(preferences ? preferences?.moderationPrefs.labelers.length <= 20 : false)
|
||||
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
||||
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
||||
useUnlikeMutation()
|
||||
@@ -114,6 +116,7 @@ let ProfileHeaderLabeler = ({
|
||||
_(
|
||||
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
logger.error(`Failed to toggle labeler like`, {message: e.message})
|
||||
}
|
||||
@@ -130,17 +133,17 @@ let ProfileHeaderLabeler = ({
|
||||
const onPressSubscribe = React.useCallback(
|
||||
() =>
|
||||
requireAuth(async (): Promise<void> => {
|
||||
if (!canSubscribe) {
|
||||
cantSubscribePrompt.open()
|
||||
return
|
||||
}
|
||||
try {
|
||||
await toggleSubscription({
|
||||
did: profile.did,
|
||||
subscribe: !isSubscribed,
|
||||
})
|
||||
} catch (e: any) {
|
||||
// setSubscriptionError(e.message)
|
||||
reset()
|
||||
if (e.message === 'MAX_LABELERS') {
|
||||
cantSubscribePrompt.open()
|
||||
return
|
||||
}
|
||||
logger.error(`Failed to subscribe to labeler`, {message: e.message})
|
||||
}
|
||||
}),
|
||||
@@ -149,8 +152,8 @@ let ProfileHeaderLabeler = ({
|
||||
toggleSubscription,
|
||||
isSubscribed,
|
||||
profile,
|
||||
canSubscribe,
|
||||
cantSubscribePrompt,
|
||||
reset,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -199,14 +202,13 @@ let ProfileHeaderLabeler = ({
|
||||
style={[
|
||||
{
|
||||
paddingVertical: gtMobile ? 12 : 10,
|
||||
backgroundColor:
|
||||
isSubscribed || !canSubscribe
|
||||
? state.hovered || state.pressed
|
||||
? t.palette.contrast_50
|
||||
: t.palette.contrast_25
|
||||
: state.hovered || state.pressed
|
||||
? tokens.color.temp_purple_dark
|
||||
: tokens.color.temp_purple,
|
||||
backgroundColor: isSubscribed
|
||||
? state.hovered || state.pressed
|
||||
? t.palette.contrast_50
|
||||
: t.palette.contrast_25
|
||||
: state.hovered || state.pressed
|
||||
? tokens.color.temp_purple_dark
|
||||
: tokens.color.temp_purple,
|
||||
},
|
||||
a.px_lg,
|
||||
a.rounded_sm,
|
||||
@@ -215,11 +217,9 @@ let ProfileHeaderLabeler = ({
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: canSubscribe
|
||||
? isSubscribed
|
||||
? t.palette.contrast_700
|
||||
: t.palette.white
|
||||
: t.palette.contrast_400,
|
||||
color: isSubscribed
|
||||
? t.palette.contrast_700
|
||||
: t.palette.white,
|
||||
},
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
@@ -317,6 +317,9 @@ let ProfileHeaderLabeler = ({
|
||||
ProfileHeaderLabeler = memo(ProfileHeaderLabeler)
|
||||
export {ProfileHeaderLabeler}
|
||||
|
||||
/**
|
||||
* Keep this in sync with the value of {@link MAX_LABELERS}
|
||||
*/
|
||||
function CantSubscribePrompt({
|
||||
control,
|
||||
}: {
|
||||
|
||||
@@ -104,7 +104,7 @@ let ProfileHeaderStandard = ({
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -126,7 +126,7 @@ let ProfileHeaderStandard = ({
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {message: String(e)})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -140,7 +140,7 @@ let ProfileHeaderStandard = ({
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unblock account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}, [_, queueUnblock, track])
|
||||
|
||||
@@ -45,14 +45,20 @@ export const Policies = ({
|
||||
const els = []
|
||||
if (tos) {
|
||||
els.push(
|
||||
<InlineLinkText key="tos" to={tos}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky Terms of Service`)}
|
||||
key="tos"
|
||||
to={tos}>
|
||||
{_(msg`Terms of Service`)}
|
||||
</InlineLinkText>,
|
||||
)
|
||||
}
|
||||
if (pp) {
|
||||
els.push(
|
||||
<InlineLinkText key="pp" to={pp}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky Privacy Policy`)}
|
||||
key="pp"
|
||||
to={pp}>
|
||||
{_(msg`Privacy Policy`)}
|
||||
</InlineLinkText>,
|
||||
)
|
||||
|
||||
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
<Text style={[t.atoms.text, !gtMobile && a.text_md]}>
|
||||
<Trans>Having trouble?</Trans>{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Contact support`)}
|
||||
to={FEEDBACK_FORM_URL({email: state.email})}
|
||||
style={[!gtMobile && a.text_md]}>
|
||||
<Trans>Contact support</Trans>
|
||||
|
||||
@@ -359,7 +359,7 @@ function Header({
|
||||
captureAction(ProgressGuideAction.Follow, dids.length)
|
||||
Toast.show(_(msg`All accounts have been followed!`))
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`An error occurred while trying to follow all`))
|
||||
Toast.show(_(msg`An error occurred while trying to follow all`), 'xmark')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
@@ -672,7 +672,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) {
|
||||
onError: e => {
|
||||
setIsProcessing(false)
|
||||
logger.error('Failed to delete invalid starter pack', {safeMessage: e})
|
||||
Toast.show(_(msg`Failed to delete starter pack`))
|
||||
Toast.show(_(msg`Failed to delete starter pack`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -74,7 +74,10 @@ function reducer(state: State, action: Action): State {
|
||||
break
|
||||
case 'AddProfile':
|
||||
if (state.profiles.length >= 51) {
|
||||
Toast.show(msg`You may only add up to 50 profiles`.message ?? '')
|
||||
Toast.show(
|
||||
msg`You may only add up to 50 profiles`.message ?? '',
|
||||
'info',
|
||||
)
|
||||
} else {
|
||||
updatedState = {...state, profiles: [...state.profiles, action.profile]}
|
||||
}
|
||||
@@ -89,7 +92,7 @@ function reducer(state: State, action: Action): State {
|
||||
break
|
||||
case 'AddFeed':
|
||||
if (state.feeds.length >= 50) {
|
||||
Toast.show(msg`You may only add up to 50 feeds`.message ?? '')
|
||||
Toast.show(msg`You may only add up to 50 feeds`.message ?? '', 'info')
|
||||
} else {
|
||||
updatedState = {...state, feeds: [...state.feeds, action.feed]}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ function WizardInner({
|
||||
onError: e => {
|
||||
logger.error('Failed to create starter pack', {safeMessage: e})
|
||||
dispatch({type: 'SetProcessing', processing: false})
|
||||
Toast.show(_(msg`Failed to create starter pack`))
|
||||
Toast.show(_(msg`Failed to create starter pack`), 'xmark')
|
||||
},
|
||||
})
|
||||
const {mutate: editStarterPack} = useEditStarterPackMutation({
|
||||
@@ -235,7 +235,7 @@ function WizardInner({
|
||||
onError: e => {
|
||||
logger.error('Failed to edit starter pack', {safeMessage: e})
|
||||
dispatch({type: 'SetProcessing', processing: false})
|
||||
Toast.show(_(msg`Failed to create starter pack`))
|
||||
Toast.show(_(msg`Failed to create starter pack`), 'xmark')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -45,9 +45,13 @@ export function useActorAutocompleteQuery(
|
||||
},
|
||||
select: React.useCallback(
|
||||
(data: AppBskyActorDefs.ProfileViewBasic[]) => {
|
||||
return computeSuggestions(data, moderationOpts || DEFAULT_MOD_OPTS)
|
||||
return computeSuggestions({
|
||||
q: prefix,
|
||||
searched: data,
|
||||
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
|
||||
})
|
||||
},
|
||||
[moderationOpts],
|
||||
[prefix, moderationOpts],
|
||||
),
|
||||
placeholderData: maintainData ? keepPreviousData : undefined,
|
||||
})
|
||||
@@ -81,19 +85,25 @@ export function useActorAutocompleteFn() {
|
||||
}
|
||||
}
|
||||
|
||||
return computeSuggestions(
|
||||
res?.data.actors,
|
||||
moderationOpts || DEFAULT_MOD_OPTS,
|
||||
)
|
||||
return computeSuggestions({
|
||||
q: query,
|
||||
searched: res?.data.actors,
|
||||
moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
|
||||
})
|
||||
},
|
||||
[queryClient, moderationOpts, agent],
|
||||
)
|
||||
}
|
||||
|
||||
function computeSuggestions(
|
||||
searched: AppBskyActorDefs.ProfileViewBasic[] = [],
|
||||
moderationOpts: ModerationOpts,
|
||||
) {
|
||||
function computeSuggestions({
|
||||
q,
|
||||
searched = [],
|
||||
moderationOpts,
|
||||
}: {
|
||||
q?: string
|
||||
searched?: AppBskyActorDefs.ProfileViewBasic[]
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
let items: AppBskyActorDefs.ProfileViewBasic[] = []
|
||||
for (const item of searched) {
|
||||
if (!items.find(item2 => item2.handle === item.handle)) {
|
||||
@@ -102,6 +112,7 @@ function computeSuggestions(
|
||||
}
|
||||
return items.filter(profile => {
|
||||
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
|
||||
return !modui.filter || isJustAMute(modui)
|
||||
const isExactMatch = q && profile.handle.toLowerCase() === q
|
||||
return isExactMatch || !modui.filter || isJustAMute(modui)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {z} from 'zod'
|
||||
|
||||
import {MAX_LABELERS} from '#/lib/constants'
|
||||
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||
import {
|
||||
preferencesQueryKey,
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useAgent} from '#/state/session'
|
||||
|
||||
const labelerInfoQueryKeyRoot = 'labeler-info'
|
||||
@@ -77,6 +81,7 @@ export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) {
|
||||
export function useLabelerSubscriptionMutation() {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const preferences = usePreferencesQuery()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn({did, subscribe}: {did: string; subscribe: boolean}) {
|
||||
@@ -86,14 +91,52 @@ export function useLabelerSubscriptionMutation() {
|
||||
subscribe: z.boolean(),
|
||||
}).parse({did, subscribe})
|
||||
|
||||
/**
|
||||
* If a user has invalid/takendown/deactivated labelers, we need to
|
||||
* remove them. We don't have a great way to do this atm on the server,
|
||||
* so we do it here.
|
||||
*
|
||||
* We also need to push validation into this method, since we need to
|
||||
* check {@link MAX_LABELERS} _after_ we've removed invalid or takendown
|
||||
* labelers.
|
||||
*/
|
||||
const labelerDids = (
|
||||
preferences.data?.moderationPrefs?.labelers ?? []
|
||||
).map(l => l.did)
|
||||
const invalidLabelers: string[] = []
|
||||
if (labelerDids.length) {
|
||||
const profiles = await agent.getProfiles({actors: labelerDids})
|
||||
if (profiles.data) {
|
||||
for (const did of labelerDids) {
|
||||
const exists = profiles.data.profiles.find(p => p.did === did)
|
||||
if (exists) {
|
||||
// profile came back but it's not a valid labeler
|
||||
if (exists.associated && !exists.associated.labeler) {
|
||||
invalidLabelers.push(did)
|
||||
}
|
||||
} else {
|
||||
// no response came back, might be deactivated or takendown
|
||||
invalidLabelers.push(did)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalidLabelers.length) {
|
||||
await Promise.all(invalidLabelers.map(did => agent.removeLabeler(did)))
|
||||
}
|
||||
|
||||
if (subscribe) {
|
||||
const labelerCount = labelerDids.length - invalidLabelers.length
|
||||
if (labelerCount >= MAX_LABELERS) {
|
||||
throw new Error('MAX_LABELERS')
|
||||
}
|
||||
await agent.addLabeler(did)
|
||||
} else {
|
||||
await agent.removeLabeler(did)
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
async onSuccess() {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -46,11 +46,14 @@ const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
|
||||
const RQKEY_ROOT = 'notification-feed'
|
||||
export function RQKEY() {
|
||||
return [RQKEY_ROOT]
|
||||
export function RQKEY(priority?: false) {
|
||||
return [RQKEY_ROOT, priority]
|
||||
}
|
||||
|
||||
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
export function useNotificationFeedQuery(opts?: {
|
||||
enabled?: boolean
|
||||
overridePriorityNotifications?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
const lastPageCountRef = useRef(0)
|
||||
const gate = useGate()
|
||||
|
||||
// false: force showing all notifications
|
||||
// undefined: let the server decide
|
||||
const priority = opts?.overridePriorityNotifications ? false : undefined
|
||||
|
||||
const query = useInfiniteQuery<
|
||||
FeedPage,
|
||||
Error,
|
||||
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
RQPageParam
|
||||
>({
|
||||
staleTime: STALE.INFINITY,
|
||||
queryKey: RQKEY(),
|
||||
queryKey: RQKEY(priority),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
let page
|
||||
if (!pageParam) {
|
||||
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
page = unreads.getCachedUnreadPage()
|
||||
}
|
||||
if (!page) {
|
||||
page = (
|
||||
await fetchPage({
|
||||
agent,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
fetchAdditionalData: true,
|
||||
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
|
||||
})
|
||||
).page
|
||||
const {page: fetchedPage} = await fetchPage({
|
||||
agent,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
fetchAdditionalData: true,
|
||||
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
|
||||
priority,
|
||||
})
|
||||
page = fetchedPage
|
||||
}
|
||||
|
||||
// if the first page has an unread, mark all read
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {until} from '#/lib/async/until'
|
||||
import {logger} from '#/logger'
|
||||
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
||||
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
|
||||
export function useNotificationsSettingsMutation() {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (keys: string[]) => {
|
||||
const enabled = keys[0] === 'enabled'
|
||||
|
||||
await agent.api.app.bsky.notification.putPreferences({
|
||||
priority: enabled,
|
||||
})
|
||||
|
||||
await until(
|
||||
5, // 5 tries
|
||||
1e3, // 1s delay between tries
|
||||
res => res.data.priority === enabled,
|
||||
() => agent.api.app.bsky.notification.listNotifications({limit: 1}),
|
||||
)
|
||||
|
||||
eagerlySetCachedPriority(queryClient, enabled)
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to save notification preferences', {
|
||||
safeMessage: err,
|
||||
})
|
||||
Toast.show(
|
||||
_(msg`Failed to save notification preferences, please try again`),
|
||||
'xmark',
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Preference saved`))
|
||||
},
|
||||
onSettled: () => {
|
||||
invalidateCachedUnreadPage()
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function eagerlySetCachedPriority(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
enabled: boolean,
|
||||
) {
|
||||
queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page: any) => {
|
||||
return {
|
||||
...page,
|
||||
priority: enabled,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export interface FeedPage {
|
||||
cursor: string | undefined
|
||||
seenAt: Date
|
||||
items: FeedNotification[]
|
||||
priority: boolean
|
||||
}
|
||||
|
||||
export interface CachedFeedPage {
|
||||
|
||||
@@ -39,10 +39,15 @@ export async function fetchPage({
|
||||
moderationOpts: ModerationOpts | undefined
|
||||
fetchAdditionalData: boolean
|
||||
shouldUngroupFollowBacks?: () => boolean
|
||||
}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
|
||||
priority?: boolean
|
||||
}): Promise<{
|
||||
page: FeedPage
|
||||
indexedAt: string | undefined
|
||||
}> {
|
||||
const res = await agent.listNotifications({
|
||||
limit,
|
||||
cursor,
|
||||
// priority,
|
||||
})
|
||||
|
||||
const indexedAt = res.data.notifications[0]?.indexedAt
|
||||
@@ -88,6 +93,7 @@ export async function fetchPage({
|
||||
cursor: res.data.cursor,
|
||||
seenAt,
|
||||
items: notifsGrouped,
|
||||
priority: res.data.priority ?? false,
|
||||
},
|
||||
indexedAt,
|
||||
}
|
||||
|
||||
@@ -78,7 +78,10 @@ export interface FeedPostSliceItem {
|
||||
uri: string
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
|
||||
reason?:
|
||||
| AppBskyFeedDefs.ReasonRepost
|
||||
| ReasonFeedSource
|
||||
| {[k: string]: unknown; $type: string}
|
||||
feedContext: string | undefined
|
||||
moderation: ModerationDecision
|
||||
parentAuthor?: AppBskyActorDefs.ProfileViewBasic
|
||||
@@ -311,7 +314,7 @@ export function usePostFeedQuery(
|
||||
if (isDiscover) {
|
||||
userActionHistory.seen(
|
||||
slice.items.map(item => ({
|
||||
feedContext: item.feedContext,
|
||||
feedContext: slice.feedContext,
|
||||
likeCount: item.post.likeCount ?? 0,
|
||||
repostCount: item.post.repostCount ?? 0,
|
||||
replyCount: item.post.replyCount ?? 0,
|
||||
@@ -323,10 +326,10 @@ export function usePostFeedQuery(
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
const feedPostSlice: FeedPostSlice = {
|
||||
_reactKey: slice._reactKey,
|
||||
_isFeedPostSlice: true,
|
||||
rootUri: slice.rootItem.post.uri,
|
||||
rootUri: slice.uri,
|
||||
isThread:
|
||||
slice.items.length > 1 &&
|
||||
slice.items.every(
|
||||
@@ -341,35 +344,42 @@ export function usePostFeedQuery(
|
||||
AppBskyFeedPost.validateRecord(item.post.record)
|
||||
.success
|
||||
) {
|
||||
const parentAuthor =
|
||||
item.reply?.parent?.author ??
|
||||
slice.items[i + 1]?.reply?.grandparentAuthor
|
||||
const parent = item.reply?.parent
|
||||
let parentAuthor:
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| undefined
|
||||
if (AppBskyFeedDefs.isPostView(parent)) {
|
||||
parentAuthor = parent.author
|
||||
}
|
||||
if (!parentAuthor) {
|
||||
parentAuthor =
|
||||
slice.items[i + 1]?.reply?.grandparentAuthor
|
||||
}
|
||||
const replyRef = item.reply
|
||||
const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
|
||||
replyRef?.parent,
|
||||
)
|
||||
|
||||
return {
|
||||
const feedPostSliceItem: FeedPostSliceItem = {
|
||||
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
|
||||
uri: item.post.uri,
|
||||
post: item.post,
|
||||
record: item.post.record,
|
||||
reason:
|
||||
i === 0 && slice.source
|
||||
? slice.source
|
||||
: item.reason,
|
||||
feedContext: item.feedContext || slice.feedContext,
|
||||
reason: slice.reason,
|
||||
feedContext: slice.feedContext,
|
||||
moderation: moderations[i],
|
||||
parentAuthor,
|
||||
isParentBlocked,
|
||||
}
|
||||
return feedPostSliceItem
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter(Boolean) as FeedPostSliceItem[],
|
||||
.filter(n => !!n),
|
||||
}
|
||||
return feedPostSlice
|
||||
})
|
||||
.filter(Boolean) as FeedPostSlice[],
|
||||
.filter(n => !!n),
|
||||
})),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
|
||||
| {type: 'nobody'}
|
||||
| {type: 'mention'}
|
||||
| {type: 'following'}
|
||||
| {type: 'list'; list: string}
|
||||
| {type: 'list'; list: unknown}
|
||||
|
||||
export function threadgateViewToSettings(
|
||||
threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
|
||||
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
|
||||
if (!record.allow?.length) {
|
||||
return [{type: 'nobody'}]
|
||||
}
|
||||
return record.allow
|
||||
const settings: ThreadgateSetting[] = record.allow
|
||||
.map(allow => {
|
||||
let setting: ThreadgateSetting | undefined
|
||||
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') {
|
||||
return {type: 'mention'}
|
||||
setting = {type: 'mention'}
|
||||
} else if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
|
||||
setting = {type: 'following'}
|
||||
} else if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
|
||||
setting = {type: 'list', list: allow.list}
|
||||
}
|
||||
if (allow.$type === 'app.bsky.feed.threadgate#followingRule') {
|
||||
return {type: 'following'}
|
||||
}
|
||||
if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
|
||||
return {type: 'list', list: allow.list}
|
||||
}
|
||||
return undefined
|
||||
return setting
|
||||
})
|
||||
.filter(Boolean) as ThreadgateSetting[]
|
||||
.filter(n => !!n)
|
||||
return settings
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {CompressedVideo, compressVideo} from 'lib/media/video/compress'
|
||||
|
||||
export function useCompressVideoMutation({
|
||||
onProgress,
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onProgress: (progress: number) => void
|
||||
onError: (e: any) => void
|
||||
onSuccess: (video: CompressedVideo) => void
|
||||
}) {
|
||||
return useMutation({
|
||||
mutationFn: async (asset: ImagePickerAsset) => {
|
||||
return await compressVideo(asset.uri, {
|
||||
onProgress: num => onProgress(trunc2dp(num)),
|
||||
})
|
||||
},
|
||||
onError,
|
||||
onSuccess,
|
||||
onMutate: () => {
|
||||
onProgress(0)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function trunc2dp(num: number) {
|
||||
return Math.trunc(num * 100) / 100
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
const UPLOAD_ENDPOINT = process.env.EXPO_PUBLIC_VIDEO_ROOT_ENDPOINT ?? ''
|
||||
|
||||
export const createVideoEndpointUrl = (
|
||||
route: string,
|
||||
params?: Record<string, string>,
|
||||
) => {
|
||||
const url = new URL(`${UPLOAD_ENDPOINT}`)
|
||||
url.pathname = route
|
||||
if (params) {
|
||||
for (const key in params) {
|
||||
url.searchParams.set(key, params[key])
|
||||
}
|
||||
}
|
||||
return url.href
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {CompressedVideo} from 'lib/media/video/compress'
|
||||
import {UploadVideoResponse} from 'lib/media/video/types'
|
||||
import {createVideoEndpointUrl} from 'state/queries/video/util'
|
||||
import {useSession} from 'state/session'
|
||||
const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
|
||||
|
||||
export const useUploadVideoMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
setProgress,
|
||||
}: {
|
||||
onSuccess: (response: UploadVideoResponse) => void
|
||||
onError: (e: any) => void
|
||||
setProgress: (progress: number) => void
|
||||
}) => {
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (video: CompressedVideo) => {
|
||||
const uri = createVideoEndpointUrl('/upload', {
|
||||
did: currentAccount!.did,
|
||||
name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
|
||||
})
|
||||
|
||||
const uploadTask = createUploadTask(
|
||||
uri,
|
||||
video.uri,
|
||||
{
|
||||
headers: {
|
||||
'dev-key': UPLOAD_HEADER,
|
||||
'content-type': 'video/mp4', // @TODO same question here. does the compression step always output mp4?
|
||||
},
|
||||
httpMethod: 'POST',
|
||||
uploadType: FileSystemUploadType.BINARY_CONTENT,
|
||||
},
|
||||
p => {
|
||||
setProgress(p.totalBytesSent / p.totalBytesExpectedToSend)
|
||||
},
|
||||
)
|
||||
const res = await uploadTask.uploadAsync()
|
||||
|
||||
if (!res?.body) {
|
||||
throw new Error('No response')
|
||||
}
|
||||
|
||||
// @TODO rm, useful for debugging/getting video cid
|
||||
console.log('[VIDEO]', res.body)
|
||||
const responseBody = JSON.parse(res.body) as UploadVideoResponse
|
||||
onSuccess(responseBody)
|
||||
return responseBody
|
||||
},
|
||||
onError,
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {CompressedVideo} from 'lib/media/video/compress'
|
||||
import {UploadVideoResponse} from 'lib/media/video/types'
|
||||
import {createVideoEndpointUrl} from 'state/queries/video/util'
|
||||
import {useSession} from 'state/session'
|
||||
const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? ''
|
||||
|
||||
export const useUploadVideoMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
setProgress,
|
||||
}: {
|
||||
onSuccess: (response: UploadVideoResponse) => void
|
||||
onError: (e: any) => void
|
||||
setProgress: (progress: number) => void
|
||||
}) => {
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (video: CompressedVideo) => {
|
||||
const uri = createVideoEndpointUrl('/upload', {
|
||||
did: currentAccount!.did,
|
||||
name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to?
|
||||
})
|
||||
|
||||
const bytes = await fetch(video.uri).then(res => res.arrayBuffer())
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
const res = (await new Promise((resolve, reject) => {
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
const progress = e.loaded / e.total
|
||||
setProgress(progress)
|
||||
})
|
||||
xhr.onloadend = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
const uploadRes = JSON.parse(
|
||||
xhr.responseText,
|
||||
) as UploadVideoResponse
|
||||
resolve(uploadRes)
|
||||
onSuccess(uploadRes)
|
||||
} else {
|
||||
reject()
|
||||
onError(new Error('Failed to upload video'))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject()
|
||||
onError(new Error('Failed to upload video'))
|
||||
}
|
||||
xhr.open('POST', uri)
|
||||
xhr.setRequestHeader('Content-Type', 'video/mp4') // @TODO how we we set the proper content type?
|
||||
// @TODO remove this header for prod
|
||||
xhr.setRequestHeader('dev-key', UPLOAD_HEADER)
|
||||
xhr.send(bytes)
|
||||
})) as UploadVideoResponse
|
||||
|
||||
// @TODO rm for prod
|
||||
console.log('[VIDEO]', res)
|
||||
return res
|
||||
},
|
||||
onError,
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import React from 'react'
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQuery} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {CompressedVideo} from 'lib/media/video/compress'
|
||||
import {VideoTooLargeError} from 'lib/media/video/errors'
|
||||
import {JobState, JobStatus} from 'lib/media/video/types'
|
||||
import {useCompressVideoMutation} from 'state/queries/video/compress-video'
|
||||
import {createVideoEndpointUrl} from 'state/queries/video/util'
|
||||
import {useUploadVideoMutation} from 'state/queries/video/video-upload'
|
||||
|
||||
type Status = 'idle' | 'compressing' | 'processing' | 'uploading' | 'done'
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'SetStatus'
|
||||
status: Status
|
||||
}
|
||||
| {
|
||||
type: 'SetProgress'
|
||||
progress: number
|
||||
}
|
||||
| {
|
||||
type: 'SetError'
|
||||
error: string | undefined
|
||||
}
|
||||
| {type: 'Reset'}
|
||||
| {type: 'SetAsset'; asset: ImagePickerAsset}
|
||||
| {type: 'SetVideo'; video: CompressedVideo}
|
||||
| {type: 'SetJobStatus'; jobStatus: JobStatus}
|
||||
|
||||
export interface State {
|
||||
status: Status
|
||||
progress: number
|
||||
asset?: ImagePickerAsset
|
||||
video: CompressedVideo | null
|
||||
jobStatus?: JobStatus
|
||||
error?: string
|
||||
}
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
let updatedState = state
|
||||
if (action.type === 'SetStatus') {
|
||||
updatedState = {...state, status: action.status}
|
||||
} else if (action.type === 'SetProgress') {
|
||||
updatedState = {...state, progress: action.progress}
|
||||
} else if (action.type === 'SetError') {
|
||||
updatedState = {...state, error: action.error}
|
||||
} else if (action.type === 'Reset') {
|
||||
updatedState = {
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
video: null,
|
||||
}
|
||||
} else if (action.type === 'SetAsset') {
|
||||
updatedState = {...state, asset: action.asset}
|
||||
} else if (action.type === 'SetVideo') {
|
||||
updatedState = {...state, video: action.video}
|
||||
} else if (action.type === 'SetJobStatus') {
|
||||
updatedState = {...state, jobStatus: action.jobStatus}
|
||||
}
|
||||
return updatedState
|
||||
}
|
||||
|
||||
export function useUploadVideo({
|
||||
setStatus,
|
||||
onSuccess,
|
||||
}: {
|
||||
setStatus: (status: string) => void
|
||||
onSuccess: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [state, dispatch] = React.useReducer(reducer, {
|
||||
status: 'idle',
|
||||
progress: 0,
|
||||
video: null,
|
||||
})
|
||||
|
||||
const {setJobId} = useUploadStatusQuery({
|
||||
onStatusChange: (status: JobStatus) => {
|
||||
// This might prove unuseful, most of the job status steps happen too quickly to even be displayed to the user
|
||||
// Leaving it for now though
|
||||
dispatch({
|
||||
type: 'SetJobStatus',
|
||||
jobStatus: status,
|
||||
})
|
||||
setStatus(status.state.toString())
|
||||
},
|
||||
onSuccess: () => {
|
||||
dispatch({
|
||||
type: 'SetStatus',
|
||||
status: 'idle',
|
||||
})
|
||||
onSuccess()
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: onVideoCompressed} = useUploadVideoMutation({
|
||||
onSuccess: response => {
|
||||
dispatch({
|
||||
type: 'SetStatus',
|
||||
status: 'processing',
|
||||
})
|
||||
setJobId(response.job_id)
|
||||
},
|
||||
onError: e => {
|
||||
dispatch({
|
||||
type: 'SetError',
|
||||
error: _(msg`An error occurred while uploading the video.`),
|
||||
})
|
||||
logger.error('Error uploading video', {safeMessage: e})
|
||||
},
|
||||
setProgress: p => {
|
||||
dispatch({type: 'SetProgress', progress: p})
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: onSelectVideo} = useCompressVideoMutation({
|
||||
onProgress: p => {
|
||||
dispatch({type: 'SetProgress', progress: p})
|
||||
},
|
||||
onError: e => {
|
||||
if (e instanceof VideoTooLargeError) {
|
||||
dispatch({
|
||||
type: 'SetError',
|
||||
error: _(msg`The selected video is larger than 100MB.`),
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SetError',
|
||||
// @TODO better error message from server, left untranslated on purpose
|
||||
error: 'An error occurred while compressing the video.',
|
||||
})
|
||||
logger.error('Error compressing video', {safeMessage: e})
|
||||
}
|
||||
},
|
||||
onSuccess: (video: CompressedVideo) => {
|
||||
dispatch({
|
||||
type: 'SetVideo',
|
||||
video,
|
||||
})
|
||||
dispatch({
|
||||
type: 'SetStatus',
|
||||
status: 'uploading',
|
||||
})
|
||||
onVideoCompressed(video)
|
||||
},
|
||||
})
|
||||
|
||||
const selectVideo = (asset: ImagePickerAsset) => {
|
||||
dispatch({
|
||||
type: 'SetAsset',
|
||||
asset,
|
||||
})
|
||||
dispatch({
|
||||
type: 'SetStatus',
|
||||
status: 'compressing',
|
||||
})
|
||||
onSelectVideo(asset)
|
||||
}
|
||||
|
||||
const clearVideo = () => {
|
||||
// @TODO cancel any running jobs
|
||||
dispatch({type: 'Reset'})
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
dispatch,
|
||||
selectVideo,
|
||||
clearVideo,
|
||||
}
|
||||
}
|
||||
|
||||
const useUploadStatusQuery = ({
|
||||
onStatusChange,
|
||||
onSuccess,
|
||||
}: {
|
||||
onStatusChange: (status: JobStatus) => void
|
||||
onSuccess: () => void
|
||||
}) => {
|
||||
const [enabled, setEnabled] = React.useState(true)
|
||||
const [jobId, setJobId] = React.useState<string>()
|
||||
|
||||
const {isLoading, isError} = useQuery({
|
||||
queryKey: ['video-upload'],
|
||||
queryFn: async () => {
|
||||
const url = createVideoEndpointUrl(`/job/${jobId}/status`)
|
||||
const res = await fetch(url)
|
||||
const status = (await res.json()) as JobStatus
|
||||
if (status.state === JobState.JOB_STATE_COMPLETED) {
|
||||
setEnabled(false)
|
||||
onSuccess()
|
||||
}
|
||||
onStatusChange(status)
|
||||
return status
|
||||
},
|
||||
enabled: Boolean(jobId && enabled),
|
||||
refetchInterval: 1500,
|
||||
})
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isError,
|
||||
setJobId: (_jobId: string) => {
|
||||
setJobId(_jobId)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
interface PostProgressState {
|
||||
progress: number
|
||||
status: 'pending' | 'success' | 'error' | 'idle'
|
||||
error?: string
|
||||
}
|
||||
|
||||
const PostProgressContext = React.createContext<PostProgressState>({
|
||||
progress: 0,
|
||||
status: 'idle',
|
||||
})
|
||||
|
||||
export function Provider() {}
|
||||
|
||||
export function usePostProgress() {
|
||||
return React.useContext(PostProgressContext)
|
||||
}
|
||||
@@ -132,6 +132,7 @@ export const SplashScreen = ({
|
||||
|
||||
function Footer() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -147,13 +148,19 @@ function Footer() {
|
||||
a.flex_1,
|
||||
t.atoms.border_contrast_medium,
|
||||
]}>
|
||||
<InlineLinkText to="https://bsky.social">
|
||||
<InlineLinkText
|
||||
label={_(msg`Learn more about Bluesky`)}
|
||||
to="https://bsky.social">
|
||||
<Trans>Business</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/blog">
|
||||
<InlineLinkText
|
||||
label={_(msg`Read the Bluesky blog`)}
|
||||
to="https://bsky.social/about/blog">
|
||||
<Trans>Blog</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText to="https://bsky.social/about/join">
|
||||
<InlineLinkText
|
||||
label={_(msg`See jobs at Bluesky`)}
|
||||
to="https://bsky.social/about/join">
|
||||
<Trans>Jobs</Trans>
|
||||
</InlineLinkText>
|
||||
|
||||
|
||||
@@ -13,10 +13,16 @@ import {
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
LayoutChangeEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
// @ts-expect-error no type definition
|
||||
import ProgressCircle from 'react-native-progress/Circle'
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
interpolateColor,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -55,6 +61,7 @@ import {
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {Gif} from '#/state/queries/tenor'
|
||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||
import {useUploadVideo} from '#/state/queries/video/video'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
@@ -70,6 +77,7 @@ import {colors, s} from 'lib/styles'
|
||||
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
||||
import {useDialogStateControlContext} from 'state/dialogs'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {State as VideoUploadState} from 'state/queries/video/video'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -96,10 +104,8 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
|
||||
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||
import {SelectVideoBtn} from './videos/SelectVideoBtn'
|
||||
import {useVideoState} from './videos/state'
|
||||
import {VideoPreview} from './videos/VideoPreview'
|
||||
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
@@ -160,14 +166,21 @@ export const ComposePost = observer(function ComposePost({
|
||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
||||
initQuote,
|
||||
)
|
||||
|
||||
const {
|
||||
video,
|
||||
onSelectVideo,
|
||||
videoPending,
|
||||
videoProcessingData,
|
||||
selectVideo,
|
||||
clearVideo,
|
||||
videoProcessingProgress,
|
||||
} = useVideoState({setError})
|
||||
state: videoUploadState,
|
||||
} = useUploadVideo({
|
||||
setStatus: (status: string) => setProcessingState(status),
|
||||
onSuccess: () => {
|
||||
if (publishOnUpload) {
|
||||
onPressPublish(true)
|
||||
}
|
||||
},
|
||||
})
|
||||
const [publishOnUpload, setPublishOnUpload] = useState(false)
|
||||
|
||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||
const [extGif, setExtGif] = useState<Gif>()
|
||||
const [labels, setLabels] = useState<string[]>([])
|
||||
@@ -275,7 +288,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
return false
|
||||
}, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
|
||||
|
||||
const onPressPublish = async () => {
|
||||
const onPressPublish = async (finishedUploading?: boolean) => {
|
||||
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
return
|
||||
}
|
||||
@@ -284,6 +297,15 @@ export const ComposePost = observer(function ComposePost({
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!finishedUploading &&
|
||||
videoUploadState.status !== 'idle' &&
|
||||
videoUploadState.asset
|
||||
) {
|
||||
setPublishOnUpload(true)
|
||||
return
|
||||
}
|
||||
|
||||
setError('')
|
||||
|
||||
if (
|
||||
@@ -388,8 +410,12 @@ export const ComposePost = observer(function ComposePost({
|
||||
: _(msg`What's up?`)
|
||||
|
||||
const canSelectImages =
|
||||
gallery.size < 4 && !extLink && !video && !videoPending
|
||||
const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video)
|
||||
gallery.size < 4 &&
|
||||
!extLink &&
|
||||
videoUploadState.status === 'idle' &&
|
||||
!videoUploadState.video
|
||||
const hasMedia =
|
||||
gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
|
||||
|
||||
const onEmojiButtonPress = useCallback(() => {
|
||||
openPicker?.(textInput.current?.getCursorPosition())
|
||||
@@ -501,7 +527,10 @@ export const ComposePost = observer(function ComposePost({
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[a.rounded_full, a.py_sm]}
|
||||
onPress={onPressPublish}>
|
||||
onPress={() => onPressPublish()}
|
||||
disabled={
|
||||
videoUploadState.status !== 'idle' && publishOnUpload
|
||||
}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
{replyTo ? (
|
||||
<Trans context="action">Reply</Trans>
|
||||
@@ -573,7 +602,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
autoFocus
|
||||
setRichText={setRichText}
|
||||
onPhotoPasted={onPhotoPasted}
|
||||
onPressPublish={onPressPublish}
|
||||
onPressPublish={() => onPressPublish()}
|
||||
onNewLink={onNewLink}
|
||||
onError={setError}
|
||||
accessible={true}
|
||||
@@ -603,29 +632,33 @@ export const ComposePost = observer(function ComposePost({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
<View style={[a.mt_md]}>
|
||||
{quote ? (
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<QuoteEmbed quote={quote} />
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
{quote.uri !== initQuote?.uri && (
|
||||
<QuoteX onRemove={() => setQuote(undefined)} />
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{videoPending && videoProcessingData ? (
|
||||
<VideoTranscodeProgress
|
||||
input={videoProcessingData}
|
||||
progress={videoProcessingProgress}
|
||||
/>
|
||||
) : (
|
||||
video && (
|
||||
) : null}
|
||||
{videoUploadState.status === 'compressing' &&
|
||||
videoUploadState.asset ? (
|
||||
<VideoTranscodeProgress
|
||||
asset={videoUploadState.asset}
|
||||
progress={videoUploadState.progress}
|
||||
/>
|
||||
) : videoUploadState.video ? (
|
||||
// remove suspense when we get rid of lazy
|
||||
<Suspense fallback={null}>
|
||||
<VideoPreview video={video} clear={clearVideo} />
|
||||
<VideoPreview
|
||||
video={videoUploadState.video}
|
||||
clear={clearVideo}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
</Animated.ScrollView>
|
||||
<SuggestedLanguage text={richtext.text} />
|
||||
|
||||
@@ -642,33 +675,37 @@ export const ComposePost = observer(function ComposePost({
|
||||
t.atoms.border_contrast_medium,
|
||||
styles.bottomBar,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
{gate('videos') && (
|
||||
<SelectVideoBtn
|
||||
onSelectVideo={onSelectVideo}
|
||||
disabled={!canSelectImages}
|
||||
{videoUploadState.status !== 'idle' ? (
|
||||
<VideoUploadToolbar state={videoUploadState} />
|
||||
) : (
|
||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
{gate('videos') && (
|
||||
<SelectVideoBtn
|
||||
onSelectVideo={selectVideo}
|
||||
disabled={!canSelectImages}
|
||||
/>
|
||||
)}
|
||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
onSelectGif={onSelectGif}
|
||||
disabled={hasMedia}
|
||||
/>
|
||||
)}
|
||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||
<SelectGifBtn
|
||||
onClose={focusTextInput}
|
||||
onSelectGif={onSelectGif}
|
||||
disabled={hasMedia}
|
||||
/>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
onPress={onEmojiButtonPress}
|
||||
style={a.p_sm}
|
||||
label={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary">
|
||||
<EmojiSmile size="lg" />
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
{!isMobile ? (
|
||||
<Button
|
||||
onPress={onEmojiButtonPress}
|
||||
style={a.p_sm}
|
||||
label={_(msg`Open emoji picker`)}
|
||||
accessibilityHint={_(msg`Open emoji picker`)}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="primary">
|
||||
<EmojiSmile size="lg" />
|
||||
</Button>
|
||||
) : null}
|
||||
</ToolbarWrapper>
|
||||
)}
|
||||
<View style={a.flex_1} />
|
||||
<SelectLangBtn />
|
||||
<CharProgress count={graphemeLength} />
|
||||
@@ -763,7 +800,7 @@ function useAnimatedBorders() {
|
||||
|
||||
const topBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledTop.value,
|
||||
[0, 1],
|
||||
@@ -773,7 +810,7 @@ function useAnimatedBorders() {
|
||||
})
|
||||
const bottomBarAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: interpolateColor(
|
||||
hasScrolledBottom.value,
|
||||
[0, 1],
|
||||
@@ -855,7 +892,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
errorIcon: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.red4,
|
||||
color: colors.red4,
|
||||
borderRadius: 30,
|
||||
@@ -891,6 +928,47 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: 7,
|
||||
paddingRight: 16,
|
||||
alignItems: 'center',
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
})
|
||||
|
||||
function ToolbarWrapper({
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
style: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
if (isWeb) return children
|
||||
return (
|
||||
<Animated.View
|
||||
style={style}
|
||||
entering={FadeIn.duration(400)}
|
||||
exiting={FadeOut.duration(400)}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoUploadToolbar({state}: {state: VideoUploadState}) {
|
||||
const t = useTheme()
|
||||
|
||||
const progress =
|
||||
state.status === 'compressing' || state.status === 'uploading'
|
||||
? state.progress
|
||||
: state.jobStatus?.progress ?? 100
|
||||
|
||||
return (
|
||||
<ToolbarWrapper
|
||||
style={[a.gap_sm, a.flex_row, a.align_center, {paddingVertical: 5}]}>
|
||||
<ProgressCircle
|
||||
size={30}
|
||||
borderWidth={1}
|
||||
borderColor={t.atoms.border_contrast_low.borderColor}
|
||||
color={t.palette.primary_500}
|
||||
progress={progress}
|
||||
/>
|
||||
<Text>{state.status}</Text>
|
||||
</ToolbarWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
|
||||
const {currentAccount} = useSession()
|
||||
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
labelMobile: {
|
||||
paddingLeft: 12,
|
||||
|
||||
@@ -17,6 +17,7 @@ export function VideoPreview({
|
||||
const player = useVideoPlayer(video.uri, player => {
|
||||
player.loop = true
|
||||
player.play()
|
||||
player.volume = 0
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,15 +9,15 @@ import {Text} from '#/components/Typography'
|
||||
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
|
||||
|
||||
export function VideoTranscodeProgress({
|
||||
input,
|
||||
asset,
|
||||
progress,
|
||||
}: {
|
||||
input: ImagePickerAsset
|
||||
asset: ImagePickerAsset
|
||||
progress: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
const aspectRatio = input.width / input.height
|
||||
const aspectRatio = asset.width / asset.height
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -29,7 +29,7 @@ export function VideoTranscodeProgress({
|
||||
a.overflow_hidden,
|
||||
{aspectRatio: isNaN(aspectRatio) ? 16 / 9 : aspectRatio},
|
||||
]}>
|
||||
<VideoTranscodeBackdrop uri={input.uri} />
|
||||
<VideoTranscodeBackdrop uri={asset.uri} />
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {compressVideo} from '#/lib/media/video/compress'
|
||||
import {logger} from '#/logger'
|
||||
import {VideoTooLargeError} from 'lib/media/video/errors'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
export function useVideoState({setError}: {setError: (error: string) => void}) {
|
||||
const {_} = useLingui()
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
const {mutate, data, isPending, isError, reset, variables} = useMutation({
|
||||
mutationFn: async (asset: ImagePickerAsset) => {
|
||||
const compressed = await compressVideo(asset.uri, {
|
||||
onProgress: num => setProgress(trunc2dp(num)),
|
||||
})
|
||||
|
||||
return compressed
|
||||
},
|
||||
onError: (e: any) => {
|
||||
// Don't log these errors in sentry, just let the user know
|
||||
if (e instanceof VideoTooLargeError) {
|
||||
Toast.show(_(msg`Videos cannot be larger than 100MB`))
|
||||
return
|
||||
}
|
||||
logger.error('Failed to compress video', {safeError: e})
|
||||
setError(_(msg`Could not compress video`))
|
||||
},
|
||||
onMutate: () => {
|
||||
setProgress(0)
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
video: data,
|
||||
onSelectVideo: mutate,
|
||||
videoPending: isPending,
|
||||
videoProcessingData: variables,
|
||||
videoError: isError,
|
||||
clearVideo: reset,
|
||||
videoProcessingProgress: progress,
|
||||
}
|
||||
}
|
||||
|
||||
function trunc2dp(num: number) {
|
||||
return Math.trunc(num * 100) / 100
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {
|
||||
useAddSavedFeedsMutation,
|
||||
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||
|
||||
export function FeedSourceCard({
|
||||
feedUri,
|
||||
@@ -125,7 +124,7 @@ export function FeedSourceCardLoaded({
|
||||
])
|
||||
Toast.show(_(msg`Added to my feeds`))
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting your server`))
|
||||
Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
|
||||
logger.error('Failed to save feed', {message: e})
|
||||
}
|
||||
}, [_, feed, pinOnSave, addSavedFeeds, isSaved])
|
||||
@@ -138,7 +137,7 @@ export function FeedSourceCardLoaded({
|
||||
// await item.unsave()
|
||||
Toast.show(_(msg`Removed from my feeds`))
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`There was an issue contacting your server`))
|
||||
Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
|
||||
logger.error('Failed to unsave feed', {message: e})
|
||||
}
|
||||
}, [_, removeFeed, savedFeedConfig])
|
||||
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
|
||||
styles.container,
|
||||
pal.border,
|
||||
style,
|
||||
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth},
|
||||
{borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
|
||||
]}
|
||||
onPress={e => {
|
||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
||||
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
|
||||
gap: 14,
|
||||
},
|
||||
border: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
headerContainer: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
|
||||
]}>
|
||||
{IS_DEV && (
|
||||
<>
|
||||
<Link to="/sys/debug">
|
||||
<Link label="View storybook" to="/sys/debug">
|
||||
<ColorPalette size="md" />
|
||||
</Link>
|
||||
</>
|
||||
|
||||
@@ -66,7 +66,10 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
|
||||
const saveImageToAlbumWithToasts = React.useCallback(
|
||||
async (uri: string) => {
|
||||
if (!permissionResponse || permissionResponse.granted === false) {
|
||||
Toast.show(_(msg`Permission to access camera roll is required.`))
|
||||
Toast.show(
|
||||
_(msg`Permission to access camera roll is required.`),
|
||||
'info',
|
||||
)
|
||||
if (permissionResponse?.canAskAgain) {
|
||||
requestPermission()
|
||||
} else {
|
||||
@@ -74,6 +77,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
|
||||
_(
|
||||
msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -83,7 +87,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
|
||||
await saveImageToMediaLibrary({uri})
|
||||
Toast.show(_(msg`Saved to your camera roll`))
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Failed to save image: ${String(e)}`))
|
||||
Toast.show(_(msg`Failed to save image: ${String(e)}`), 'xmark')
|
||||
}
|
||||
},
|
||||
[permissionResponse, requestPermission, _],
|
||||
|
||||
@@ -14,7 +14,6 @@ import {RichText as RichTextCom} from '#/components/RichText'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const ListCard = ({
|
||||
testID,
|
||||
@@ -134,7 +133,7 @@ export const ListCard = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
outerNoBorder: {
|
||||
|
||||
@@ -77,7 +77,7 @@ export function Component({}: {}) {
|
||||
const onCopy = React.useCallback(() => {
|
||||
if (appPassword) {
|
||||
setStringAsync(appPassword)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
setWasCopied(true)
|
||||
}
|
||||
}, [appPassword, _])
|
||||
@@ -93,7 +93,7 @@ export function Component({}: {}) {
|
||||
_(
|
||||
msg`Please enter a name for your app password. All spaces is not allowed.`,
|
||||
),
|
||||
'times',
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -101,13 +101,13 @@ export function Component({}: {}) {
|
||||
if (name.length < 4) {
|
||||
Toast.show(
|
||||
_(msg`App Password names must be at least 4 characters long.`),
|
||||
'times',
|
||||
'xmark',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (passwords?.find(p => p.name === name)) {
|
||||
Toast.show(_(msg`This name is already in use`), 'times')
|
||||
Toast.show(_(msg`This name is already in use`), 'xmark')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,11 +116,11 @@ export function Component({}: {}) {
|
||||
if (newPassword) {
|
||||
setAppPassword(newPassword.password)
|
||||
} else {
|
||||
Toast.show(_(msg`Failed to create app password.`), 'times')
|
||||
Toast.show(_(msg`Failed to create app password.`), 'xmark')
|
||||
// TODO: better error handling (?)
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.show(_(msg`Failed to create app password.`), 'times')
|
||||
Toast.show(_(msg`Failed to create app password.`), 'xmark')
|
||||
logger.error('Failed to create app password', {message: e})
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,7 @@ export function Component({}: {}) {
|
||||
_(
|
||||
msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user