Merge remote-tracking branch 'origin/main' into silence-of-the-words

* origin/main: (37 commits)
  Only show "followed you back" when appropriate (#4849)
  [Web] Retrigger onEndReached if needed when content height changes (#4859)
  Remove unused NoopFeedTuner (#4856)
  useDedupe callback (#4855)
  [Video] Uploads (#4754)
  Make label required in link components (#4844)
  Boolean filter improvement alternative: TS upgrade (#4840)
  Add label to profile card (#4843)
  Improve a11y on noty feed (#4842)
  Add labels in feed card (#4836)
  Add labels to mod details dialog (#4839)
  Add labels to a few missing places (#4838)
  Add labels in list card (#4837)
  Refactor feed slices (#4834)
  `true` (#4833)
  Replace `import hairlineWidth =` with const (#4831)
  [Videos] Video player - PR #1 - basic player (#4731)
  Bump 1.90 (#4832)
  Fix sloppy filter(Boolean) types (#4830)
  Fuggedaboudit (#4829)
  ...
This commit is contained in:
Eric Bailey
2024-07-31 16:57:44 -05:00
159 changed files with 13317 additions and 11027 deletions
+3 -2
View File
@@ -132,7 +132,8 @@ appId: xyz.blueskyweb.app
id: "feedItem-by-bob.test" id: "feedItem-by-bob.test"
- tapOn: - tapOn:
id: "e2eGotoFeeds" id: "e2eGotoFeeds"
- tapOn: "Good Ppl" - tapOn:
id: "saved-feed-Good Ppl"
- assertVisible: - assertVisible:
id: "feedItem-by-bob.test" id: "feedItem-by-bob.test"
- tapOn: - tapOn:
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector" id: "profilePager-selector"
direction: LEFT direction: LEFT
- tapOn: - tapOn:
id: "profilePager-selector-5" id: "profilePager-selector-6"
- tapOn: "Good Ppl" - tapOn: "Good Ppl"
- tapOn: - tapOn:
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="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"/></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 374 B

+1
View File
@@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric) e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric) e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric) e.GET("/notifications", server.WebGeneric)
e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric) e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric) e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric) e.GET("/moderation/modlists", server.WebGeneric)
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.89.0", "version": "1.90.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -143,6 +143,7 @@
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"history": "^5.3.0", "history": "^5.3.0",
"hls.js": "^1.5.11",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lande": "^1.0.10", "lande": "^1.0.10",
@@ -269,7 +270,7 @@
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.3.3", "typescript": "^5.5.4",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1", "webpack-bundle-analyzer": "^4.10.1",
+12
View File
@@ -12,3 +12,15 @@ index bb74e80..0aa0202 100644
Map<String, Object> constants = new HashMap<>(3); Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>()); constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
index 109d3fe..c7fce9e 100644
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
@@ -1,5 +1,7 @@
import bytesToUuid from './lib/bytesToUuid';
import { Uuidv5Namespace } from './uuid.types';
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
+ensureNativeModulesAreInstalled();
const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4() {
+20
View File
@@ -0,0 +1,20 @@
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
setShowPreviousButton(!requireLinearPlayback)
setShowNextButton(!requireLinearPlayback)
setTimeBarInteractive(requireLinearPlayback)
+ setShowSubtitleButton(true)
}
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
+ val fullscreenButton =
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
fullscreenButton?.visibility = if (visible) {
android.view.View.VISIBLE
} else {
+45 -39
View File
@@ -23,10 +23,12 @@ import {
} from '#/lib/statsig/statsig' } from '#/lib/statsig/statsig'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' 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 SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {TestCtrls} from '#/view/com/testing/TestCtrls' 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 * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell' import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf} from '#/alf'
@@ -58,8 +61,6 @@ import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {Provider as TourProvider} from '#/tours' import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@@ -95,7 +96,10 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { 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}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}> <Splash isReady={isReady && hasCheckedReferrer}>
<RootSiblingParent> <ActiveVideoProvider>
<React.Fragment <RootSiblingParent>
// Resets the entire tree below when it changes: <React.Fragment
key={currentAccount?.did}> // Resets the entire tree below when it changes:
<QueryProvider currentDid={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<MessagesProvider> <StatsigProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<TourProvider> <MutedThreadsProvider>
<ProgressGuideProvider> <TourProvider>
<GestureHandlerRootView <ProgressGuideProvider>
style={s.h100pct}> <GestureHandlerRootView
<TestCtrls /> style={s.h100pct}>
<Shell /> <TestCtrls />
</GestureHandlerRootView> <Shell />
</ProgressGuideProvider> </GestureHandlerRootView>
</TourProvider> </ProgressGuideProvider>
</MutedThreadsProvider> </TourProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</StatsigProvider> </MessagesProvider>
</QueryProvider> </StatsigProvider>
</React.Fragment> </QueryProvider>
</RootSiblingParent> </React.Fragment>
</RootSiblingParent>
</ActiveVideoProvider>
</Splash> </Splash>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
+42 -36
View File
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
import {QueryProvider} from '#/lib/react-query' import {QueryProvider} from '#/lib/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {ThemeProvider} from '#/lib/ThemeContext' import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y' import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs' import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
import {Provider as InvitesStateProvider} from '#/state/invites' import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox' import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages' 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 ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' 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 * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web' import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' 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 PortalProvider} from '#/components/Portal'
import {Provider as TourProvider} from '#/tours' import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
function InnerApp() { function InnerApp() {
const [isReady, setIsReady] = React.useState(false) const [isReady, setIsReady] = React.useState(false)
@@ -77,7 +78,10 @@ function InnerApp() {
useEffect(() => { useEffect(() => {
return listenSessionDropped(() => { 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}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<RootSiblingParent> <RootSiblingParent>
<React.Fragment <ActiveVideoProvider>
// Resets the entire tree below when it changes: <React.Fragment
key={currentAccount?.did}> // Resets the entire tree below when it changes:
<QueryProvider currentDid={currentAccount?.did}> key={currentAccount?.did}>
<StatsigProvider> <QueryProvider currentDid={currentAccount?.did}>
<MessagesProvider> <StatsigProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} <MessagesProvider>
<LabelDefsProvider> {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<ModerationOptsProvider> <LabelDefsProvider>
<LoggedOutViewProvider> <ModerationOptsProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<BackgroundNotificationPreferencesProvider> <UnreadNotifsProvider>
<MutedThreadsProvider> <BackgroundNotificationPreferencesProvider>
<SafeAreaProvider> <MutedThreadsProvider>
<TourProvider> <SafeAreaProvider>
<ProgressGuideProvider> <TourProvider>
<Shell /> <ProgressGuideProvider>
</ProgressGuideProvider> <Shell />
</TourProvider> </ProgressGuideProvider>
</SafeAreaProvider> </TourProvider>
</MutedThreadsProvider> </SafeAreaProvider>
</BackgroundNotificationPreferencesProvider> </MutedThreadsProvider>
</UnreadNotifsProvider> </BackgroundNotificationPreferencesProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</ModerationOptsProvider> </LoggedOutViewProvider>
</LabelDefsProvider> </ModerationOptsProvider>
</MessagesProvider> </LabelDefsProvider>
</StatsigProvider> </MessagesProvider>
</QueryProvider> </StatsigProvider>
</React.Fragment> </QueryProvider>
<ToastContainer /> </React.Fragment>
<ToastContainer />
</ActiveVideoProvider>
</RootSiblingParent> </RootSiblingParent>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
+6
View File
@@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists' import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound' import {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications' import {NotificationsScreen} from './view/screens/Notifications'
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread' import {PostThreadScreen} from './view/screens/PostThread'
@@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen} getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}} options={{title: title(msg`Chat settings`), requireAuth: true}}
/> />
<Stack.Screen
name="NotificationsSettings"
getComponent={() => NotificationsSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen <Stack.Screen
name="Feeds" name="Feeds"
getComponent={() => FeedsScreen} getComponent={() => FeedsScreen}
+8 -6
View File
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
import * as tokens from '#/alf/tokens' import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform' import {native, web} from '#/alf/util/platform'
import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = { export const atoms = {
/* /*
@@ -145,6 +144,9 @@ export const atoms = {
flex_shrink: { flex_shrink: {
flexShrink: 1, flexShrink: 1,
}, },
flex_shrink_0: {
flexShrink: 0,
},
justify_start: { justify_start: {
justifyContent: 'flex-start', justifyContent: 'flex-start',
}, },
@@ -281,19 +283,19 @@ export const atoms = {
borderWidth: 0, borderWidth: 0,
}, },
border: { border: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
}, },
border_t: { border_t: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
border_b: { border_b: {
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
}, },
border_l: { border_l: {
borderLeftWidth: hairlineWidth, borderLeftWidth: StyleSheet.hairlineWidth,
}, },
border_r: { border_r: {
borderRightWidth: hairlineWidth, borderRightWidth: StyleSheet.hairlineWidth,
}, },
/* /*
+3
View File
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
AccessibilityProps & AccessibilityProps &
VariantProps & { VariantProps & {
testID?: string testID?: string
/**
* For a11y, try to make this descriptive and clear
*/
label: string label: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
hoverStyle?: StyleProp<ViewStyle> hoverStyle?: StyleProp<ViewStyle>
+10 -6
View File
@@ -40,7 +40,7 @@ type Props = {
export function Default(props: Props) { export function Default(props: Props) {
const {view} = props const {view} = props
return ( return (
<Link label={view.displayName} {...props}> <Link {...props}>
<Outer> <Outer>
<Header> <Header>
<Avatar src={view.avatar} /> <Avatar src={view.avatar} />
@@ -58,7 +58,7 @@ export function Link({
view, view,
children, children,
...props ...props
}: Props & Omit<LinkProps, 'to'>) { }: Props & Omit<LinkProps, 'to' | 'label'>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const href = React.useMemo(() => { const href = React.useMemo(() => {
@@ -70,7 +70,11 @@ export function Link({
}, [view, queryClient]) }, [view, queryClient])
return ( return (
<InternalLink to={href} style={[a.flex_col]} {...props}> <InternalLink
label={view.displayName}
to={href}
style={[a.flex_col]}
{...props}>
{children} {children}
</InternalLink> </InternalLink>
) )
@@ -264,9 +268,9 @@ function SaveButtonInner({
]) ])
} }
Toast.show(_(msg`Feeds updated!`)) Toast.show(_(msg`Feeds updated!`))
} catch (e: any) { } catch (err: any) {
logger.error(e, {context: `FeedCard: failed to update feeds`, pin}) logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`)) Toast.show(_(msg`Failed to update feeds`), 'xmark')
} }
}, },
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type], [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
+9 -3
View File
@@ -190,7 +190,7 @@ export function SuggestedFollows() {
{profiles.slice(0, maxLength).map(profile => ( {profiles.slice(0, maxLength).map(profile => (
<ProfileCard.Link <ProfileCard.Link
key={profile.did} key={profile.did}
did={profile.handle} profile={profile}
onPress={() => { onPress={() => {
logEvent('feed:interstitial:profileCard:press', {}) logEvent('feed:interstitial:profileCard:press', {})
}} }}
@@ -266,7 +266,10 @@ export function SuggestedFollows() {
a.pt_xs, a.pt_xs,
a.gap_md, 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> <Trans>Browse more suggestions</Trans>
</InlineLinkText> </InlineLinkText>
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
@@ -396,7 +399,10 @@ export function SuggestedFeeds() {
a.pt_xs, a.pt_xs,
a.gap_md, 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> <Trans>Browse more suggestions</Trans>
</InlineLinkText> </InlineLinkText>
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
+4 -9
View File
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
> & { > & {
testID?: string testID?: string
/**
* Label for a11y. Defaults to the href.
*/
label?: string
/** /**
* The React Navigation `StackAction` to perform when the link is pressed. * The React Navigation `StackAction` to perform when the link is pressed.
*/ */
@@ -197,7 +192,7 @@ export function useLink({
} }
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> & 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 * A interactive element that renders as a `<a>` tag on the web. On mobile it
@@ -224,7 +219,6 @@ export function Link({
return ( return (
<Button <Button
label={href}
{...rest} {...rest}
style={[a.justify_start, flatten(rest.style)]} style={[a.justify_start, flatten(rest.style)]}
role="link" role="link"
@@ -249,7 +243,8 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren< export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> > &
Pick<ButtonProps, 'label'>
export function InlineLinkText({ export function InlineLinkText({
children, children,
@@ -291,7 +286,7 @@ export function InlineLinkText({
<Text <Text
selectable={selectable} selectable={selectable}
accessibilityHint="" accessibilityHint=""
accessibilityLabel={label || href} accessibilityLabel={label}
{...rest} {...rest}
style={[ style={[
{color: t.palette.primary_500}, {color: t.palette.primary_500},
+3 -3
View File
@@ -44,7 +44,7 @@ type Props = {
export function Default(props: Props) { export function Default(props: Props) {
const {view, showPinButton} = props const {view, showPinButton} = props
return ( return (
<Link label={view.name} {...props}> <Link {...props}>
<Outer> <Outer>
<Header> <Header>
<Avatar src={view.avatar} /> <Avatar src={view.avatar} />
@@ -67,7 +67,7 @@ export function Link({
view, view,
children, children,
...props ...props
}: Props & Omit<LinkProps, 'to'>) { }: Props & Omit<LinkProps, 'to' | 'label'>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const href = React.useMemo(() => { const href = React.useMemo(() => {
@@ -79,7 +79,7 @@ export function Link({
}, [view, queryClient]) }, [view, queryClient])
return ( return (
<InternalLink to={href} {...props}> <InternalLink label={view.name} to={href} {...props}>
{children} {children}
</InternalLink> </InternalLink>
) )
+1 -1
View File
@@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
return ( return (
<Error <Error
title={errorTitle ?? _(msg`Oops!`)} title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)} message={errorMessage ?? _(msg`Something went wrong!`)}
onRetry={onRetry} onRetry={onRetry}
onGoBack={onGoBack} onGoBack={onGoBack}
sideBorders={sideBorders} sideBorders={sideBorders}
+18 -10
View File
@@ -36,7 +36,7 @@ export function Default({
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) { }) {
return ( return (
<Link did={profile.did}> <Link profile={profile}>
<Card <Card
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
@@ -96,16 +96,24 @@ export function Header({
} }
export function Link({ export function Link({
did, profile,
children, children,
style, style,
...rest ...rest
}: {did: string} & Omit<LinkProps, 'to'>) { }: {
profile: AppBskyActorDefs.ProfileViewDetailed
} & Omit<LinkProps, 'to' | 'label'>) {
const {_} = useLingui()
return ( return (
<InternalLink <InternalLink
label={_(
msg`View ${
profile.displayName || sanitizeHandle(profile.handle)
}'s profile`,
)}
to={{ to={{
screen: 'Profile', screen: 'Profile',
params: {name: did}, params: {name: profile.did},
}} }}
style={[a.flex_col, style]} style={[a.flex_col, style]}
{...rest}> {...rest}>
@@ -306,9 +314,9 @@ export function FollowButtonInner({
)}`, )}`,
), ),
) )
} catch (e: any) { } catch (err: any) {
if (e?.name !== 'AbortError') { if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
@@ -326,9 +334,9 @@ export function FollowButtonInner({
)}`, )}`,
), ),
) )
} catch (e: any) { } catch (err: any) {
if (e?.name !== 'AbortError') { if (err?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
} }
+4 -1
View File
@@ -67,7 +67,10 @@ export function QrCodeDialog({
try { try {
await createAssetAsync(`file://${uri}`) await createAssetAsync(`file://${uri}`)
} catch (e: unknown) { } 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}) logger.error('Failed to save QR code', {error: e})
return return
} }
+2 -1
View File
@@ -66,6 +66,7 @@ function ShareDialogInner({
if (!res) { if (!res) {
Toast.show( Toast.show(
_(msg`You must grant access to your photo library to save the image.`), _(msg`You must grant access to your photo library to save the image.`),
'xmark',
) )
return return
} }
@@ -75,7 +76,7 @@ function ShareDialogInner({
Toast.show(_(msg`Image saved to your camera roll!`)) Toast.show(_(msg`Image saved to your camera roll!`))
control.close() control.close()
} catch (e: unknown) { } 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}) logger.error('Failed to save QR code', {error: e})
return return
} }
+1
View File
@@ -115,6 +115,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
_( _(
msg`There was an issue. Please check your internet connection and try again.`, msg`There was an issue. Please check your internet connection and try again.`,
), ),
'xmark',
) )
logger.error('Failed to edit threadgate', {message: err}) logger.error('Failed to edit threadgate', {message: err})
} }
+3 -1
View File
@@ -74,7 +74,9 @@ function DialogContent({
const onPressAudience = (setting: ThreadgateSetting) => { const onPressAudience = (setting: ThreadgateSetting) => {
// remove nobody // remove nobody
let newSelected = draft.filter(v => v.type !== 'nobody') let newSelected: ThreadgateSetting[] = draft.filter(
v => v.type !== 'nobody',
)
// toggle // toggle
const i = newSelected.findIndex(v => isEqual(v, setting)) const i = newSelected.findIndex(v => isEqual(v, setting))
if (i === -1) { if (i === -1) {
@@ -42,6 +42,7 @@ export function BlockedByListDialog({
<React.Fragment key={block.source.list.uri}> <React.Fragment key={block.source.list.uri}>
{i === 0 ? null : ', '} {i === 0 ? null : ', '}
<InlineLinkText <InlineLinkText
label={block.source.list.name}
to={listUriToHref(block.source.list.uri)} to={listUriToHref(block.source.list.uri)}
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{block.source.list.name} {block.source.list.name}
+1 -1
View File
@@ -85,7 +85,7 @@ let ConvoMenu = ({
} }
}, },
onError: () => { onError: () => {
Toast.show(_(msg`Could not mute chat`)) Toast.show(_(msg`Could not mute chat`), 'xmark')
}, },
}) })
+1 -1
View File
@@ -36,7 +36,7 @@ export function LeaveConvoPrompt({
} }
}, },
onError: () => { onError: () => {
Toast.show(_(msg`Could not leave chat`)) Toast.show(_(msg`Could not leave chat`), 'xmark')
}, },
}) })
+1 -1
View File
@@ -54,7 +54,7 @@ export let MessageMenu = ({
) )
Clipboard.setStringAsync(str) Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, message.text, message.facets]) }, [_, message.text, message.facets])
const onPressTranslateMessage = React.useCallback(() => { const onPressTranslateMessage = React.useCallback(() => {
@@ -140,6 +140,7 @@ function HeaderReady({
userBlock?: ModerationCause userBlock?: ModerationCause
} }
}) { }) {
const {_} = useLingui()
const t = useTheme() const t = useTheme()
const convoState = useConvo() const convoState = useConvo()
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
@@ -156,6 +157,7 @@ function HeaderReady({
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}> <View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
<Link <Link
label={_(msg`View ${displayName}'s profile`)}
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]} style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}> to={makeProfileLink(profile)}>
<View style={[a.pt_2xs]}> <View style={[a.pt_2xs]}>
+1 -1
View File
@@ -57,7 +57,7 @@ function DialogInner({
const [initialized, setInitialzed] = React.useState(false) const [initialized, setInitialzed] = React.useState(false)
const {mutate: updateDeclaration} = useUpdateActorDeclaration({ const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => { onError: () => {
Toast.show(_(msg`Failed to update settings`)) Toast.show(_(msg`Failed to update settings`), 'xmark')
}, },
}) })
+3 -1
View File
@@ -2,6 +2,7 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import {FAB} from '#/view/com/util/fab/FAB' import {FAB} from '#/view/com/util/fab/FAB'
@@ -31,7 +32,8 @@ export function NewChat({
logEvent('chat:open', {logContext: 'NewChatDialog'}) logEvent('chat:open', {logContext: 'NewChatDialog'})
}, },
onError: error => { 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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@@ -43,7 +44,11 @@ function SendViaChatDialogInner({
logEvent('chat:open', {logContext: 'SendViaChatDialog'}) logEvent('chat:open', {logContext: 'SendViaChatDialog'})
}, },
onError: error => { 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',
)
}, },
}) })
+20 -14
View File
@@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import {Pressable, View, ViewStyle} from 'react-native' 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 {HITSLOP_10} from 'lib/constants'
import { import {
atoms as a, atoms as a,
@@ -244,6 +246,7 @@ export function LabelText({
<Text <Text
style={[ style={[
a.font_bold, a.font_bold,
a.leading_tight,
{ {
userSelect: 'none', userSelect: 'none',
color: disabled color: disabled
@@ -251,7 +254,7 @@ export function LabelText({
: t.atoms.text_contrast_high.color, : t.atoms.text_contrast_high.color,
}, },
native({ native({
paddingTop: 3, paddingTop: 2,
}), }),
flatten(style), flatten(style),
]}> ]}>
@@ -344,10 +347,10 @@ export function Checkbox() {
style={[ style={[
a.justify_center, a.justify_center,
a.align_center, a.align_center,
a.border,
a.rounded_xs, a.rounded_xs,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 20, width: 20,
}, },
@@ -375,34 +378,35 @@ export function Switch() {
<View <View
style={[ style={[
a.relative, a.relative,
a.border,
a.rounded_full, a.rounded_full,
t.atoms.bg, t.atoms.bg,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 30, width: 32,
padding: 2,
}, },
baseStyles, baseStyles,
hovered ? baseHoverStyles : {}, hovered ? baseHoverStyles : {},
]}> ]}>
<View <Animated.View
layout={LinearTransition.duration(100)}
style={[ style={[
a.absolute,
a.rounded_full, a.rounded_full,
{ {
height: 12, height: 14,
width: 12, width: 14,
top: 3,
left: 3,
backgroundColor: t.palette.contrast_400,
}, },
selected selected
? { ? {
backgroundColor: t.palette.primary_500, backgroundColor: t.palette.primary_500,
left: 13, alignSelf: 'flex-end',
} }
: {}, : {
backgroundColor: t.palette.contrast_400,
alignSelf: 'flex-start',
},
indicatorStyles, indicatorStyles,
]} ]}
/> />
@@ -428,10 +432,10 @@ export function Radio() {
style={[ style={[
a.justify_center, a.justify_center,
a.align_center, a.align_center,
a.border,
a.rounded_full, a.rounded_full,
t.atoms.border_contrast_high, t.atoms.border_contrast_high,
{ {
borderWidth: 1,
height: 20, height: 20,
width: 20, width: 20,
}, },
@@ -456,3 +460,5 @@ export function Radio() {
</View> </View>
) )
} }
export const Platform = isNative ? Switch : Checkbox
+2 -2
View File
@@ -32,7 +32,7 @@ export function useFollowMethods({
} catch (e: any) { } catch (e: any) {
logger.error(`useFollowMethods: failed to follow`, {message: String(e)}) logger.error(`useFollowMethods: failed to follow`, {message: String(e)})
if (e?.name !== 'AbortError') { 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), message: String(e),
}) })
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(_(msg`An issue occurred, please try again.`)) Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
} }
} }
}) })
-5
View File
@@ -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',
})
+9
View File
@@ -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 disabled?: boolean
labelerDid?: string labelerDid?: string
}) { }) {
const {i18n} = useLingui() const {_, i18n} = useLingui()
const t = useTheme() const t = useTheme()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
@@ -243,7 +243,10 @@ export function LabelerLabelPreference({
) : isGlobalLabel ? ( ) : isGlobalLabel ? (
<Trans> <Trans>
Configured in{' '} Configured in{' '}
<InlineLinkText to="/moderation" style={a.text_sm}> <InlineLinkText
label={_(msg`moderation settings`)}
to="/moderation"
style={a.text_sm}>
moderation settings moderation settings
</InlineLinkText> </InlineLinkText>
. .
+11 -5
View File
@@ -128,6 +128,9 @@ function Label({
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {labeler, strings} = useLabelInfo(label) const {labeler, strings} = useLabelInfo(label)
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src
return ( return (
<View <View
style={[ style={[
@@ -169,13 +172,12 @@ function Label({
<Trans> <Trans>
Source:{' '} Source:{' '}
<InlineLinkText <InlineLinkText
label={sourceName}
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()}> onPress={() => control.close()}>
{labeler {sourceName}
? sanitizeHandle(labeler.creator.handle, '@')
: label.src}
</InlineLinkText> </InlineLinkText>
</Trans> </Trans>
)} )}
@@ -203,6 +205,9 @@ function AppealForm({
const isAccountReport = 'did' in subject const isAccountReport = 'did' in subject
const agent = useAgent() const agent = useAgent()
const gate = useGate() const gate = useGate()
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src
const {mutate, isPending} = useMutation({ const {mutate, isPending} = useMutation({
mutationFn: async () => { mutationFn: async () => {
@@ -241,7 +246,7 @@ function AppealForm({
}, },
onError: err => { onError: err => {
logger.error('Failed to submit label appeal', {message: 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: () => { onSuccess: () => {
control.close() control.close()
@@ -260,12 +265,13 @@ function AppealForm({
<Trans> <Trans>
This appeal will be sent to{' '} This appeal will be sent to{' '}
<InlineLinkText <InlineLinkText
label={sourceName}
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()} onPress={() => control.close()}
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} {sourceName}
</InlineLinkText> </InlineLinkText>
. .
</Trans> </Trans>
@@ -54,7 +54,10 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} 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} {list.name}
</InlineLinkText>{' '} </InlineLinkText>{' '}
list which you have blocked. list which you have blocked.
@@ -83,7 +86,10 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} 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} {list.name}
</InlineLinkText>{' '} </InlineLinkText>{' '}
list which you have muted. list which you have muted.
@@ -127,10 +133,11 @@ function ModerationDetailsDialogInner({
<Trans> <Trans>
This label was applied by{' '} This label was applied by{' '}
<InlineLinkText <InlineLinkText
label={desc.source || _(msg`an unknown labeler`)}
to={makeProfileLink({did: modcause.label.src, handle: ''})} to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()} onPress={() => control.close()}
style={a.text_md}> style={a.text_md}>
{desc.source} {desc.source || _(msg`an unknown labeler`)}
</InlineLinkText> </InlineLinkText>
. .
</Trans> </Trans>
+69 -70
View File
@@ -14,29 +14,33 @@ export type FeedTunerFn = (
slices: FeedViewPostsSlice[], slices: FeedViewPostsSlice[],
) => 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 { export class FeedViewPostsSlice {
_reactKey: string _reactKey: string
isFlattenedReply = false _feedPost: FeedViewPost
items: FeedSliceItem[]
constructor(public items: FeedViewPost[]) { constructor(feedPost: FeedViewPost) {
const item = items[0] this._feedPost = feedPost
this._reactKey = `slice-${item.post.uri}-${ this._reactKey = `slice-${feedPost.post.uri}-${
item.reason?.indexedAt || item.post.indexedAt feedPost.reason?.indexedAt || feedPost.post.indexedAt
}` }`
this.items = [toSliceItem(feedPost)]
} }
get uri() { get uri() {
if (this.isFlattenedReply) { return this._feedPost.post.uri
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
} }
get isThread() { get isThread() {
@@ -48,31 +52,42 @@ export class FeedViewPostsSlice {
) )
} }
get isFullThread() { get isQuotePost() {
return this.isThread && !this.items[0].reply const embed = this._feedPost.post.embed
} return (
AppBskyEmbedRecord.isView(embed) ||
get rootItem() { AppBskyEmbedRecordWithMedia.isView(embed)
if (this.isFlattenedReply) { )
return this.items[1]
}
return this.items[0]
} }
get isReply() { get isReply() {
return ( return (
AppBskyFeedPost.isRecord(this.rootItem.post.record) && AppBskyFeedPost.isRecord(this._feedPost.post.record) &&
!!this.rootItem.post.record.reply !!this._feedPost.post.record.reply
) )
} }
get source(): ReasonFeedSource | undefined { get reason() {
return this.items.find(item => '__source' in item && !!item.__source) return '__source' in this._feedPost
?.__source as ReasonFeedSource ? (this._feedPost.__source as ReasonFeedSource)
: this._feedPost.reason
} }
get feedContext() { 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) { containsUri(uri: string) {
@@ -97,40 +112,30 @@ export class FeedViewPostsSlice {
if (this.items[0].reply) { if (this.items[0].reply) {
const reply = this.items[0].reply const reply = this.items[0].reply
if (AppBskyFeedDefs.isPostView(reply.parent)) { if (AppBskyFeedDefs.isPostView(reply.parent)) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: reply.parent}) this.items.splice(0, 0, {post: reply.parent})
} }
} }
} }
isFollowingAllAuthors(userDid: string) { isFollowingAllAuthors(userDid: string) {
const item = this.rootItem const feedPost = this._feedPost
if (item.post.author.did === userDid) { if (feedPost.post.author.did === userDid) {
return true return true
} }
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) { if (AppBskyFeedDefs.isPostView(feedPost.reply?.parent)) {
const parent = item.reply?.parent const parent = feedPost.reply?.parent
if (parent?.author.did === userDid) { if (parent?.author.did === userDid) {
return true return true
} }
return ( return (
parent?.author.viewer?.following && item.post.author.viewer?.following parent?.author.viewer?.following &&
feedPost.post.author.viewer?.following
) )
} }
return false return false
} }
} }
export class NoopFeedTuner {
reset() {}
tune(
feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean},
): FeedViewPostsSlice[] {
return feed.map(item => new FeedViewPostsSlice([item]))
}
}
export class FeedTuner { export class FeedTuner {
seenKeys: Set<string> = new Set() seenKeys: Set<string> = new Set()
seenUris: Set<string> = new Set() seenUris: Set<string> = new Set()
@@ -165,7 +170,7 @@ export class FeedTuner {
}) })
if (maintainOrder) { if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item])) slices = feed.map(item => new FeedViewPostsSlice(item))
} else { } else {
// arrange the posts into thread slices // arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) { for (let i = feed.length - 1; i >= 0; i--) {
@@ -192,7 +197,7 @@ export class FeedTuner {
} }
} }
slices.unshift(new FeedViewPostsSlice([item])) slices.unshift(new FeedViewPostsSlice(item))
} }
} }
@@ -215,7 +220,7 @@ export class FeedTuner {
// turn non-threads with reply parents into threads // turn non-threads with reply parents into threads
for (const slice of slices) { 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 const reply = slice.items[0].reply
if ( if (
AppBskyFeedDefs.isPostView(reply.parent) && AppBskyFeedDefs.isPostView(reply.parent) &&
@@ -256,8 +261,7 @@ export class FeedTuner {
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const reason = slices[i].rootItem.reason if (slices[i].isRepost) {
if (AppBskyFeedDefs.isReasonRepost(reason)) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@@ -266,11 +270,7 @@ export class FeedTuner {
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const embed = slices[i].rootItem.post.embed if (slices[i].isQuotePost) {
if (
AppBskyEmbedRecord.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed)
) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@@ -315,19 +315,18 @@ export class FeedTuner {
// remove any replies without at least minLikes likes // remove any replies without at least minLikes likes
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const slice = slices[i] const slice = slices[i]
if (slice.isFullThread || !slice.isReply) { if (slice.isReply) {
continue if (slice.isThread && slice.includesThreadRoot) {
} continue
}
const item = slice.rootItem if (slice.isRepost) {
const isRepost = Boolean(item.reason) continue
if (isRepost) { }
continue if (slice.likeCount < minLikes) {
} slices.splice(i, 1)
if ((item.post.likeCount || 0) < minLikes) { } else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
slices.splice(i, 1) slices.splice(i, 1)
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) { }
slices.splice(i, 1)
} }
} }
return slices return slices
+1 -1
View File
@@ -251,7 +251,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
dryRun: false, dryRun: false,
maintainOrder: true, maintainOrder: true,
}) })
res.data.feed = slices.map(slice => slice.rootItem) res.data.feed = slices.map(slice => slice._feedPost)
return res return res
} }
} }
+4
View File
@@ -54,6 +54,10 @@ interface PostOpts {
uri: string uri: string
cid: string cid: string
} }
video?: {
uri: string
cid: string
}
extLink?: ExternalEmbedDraft extLink?: ExternalEmbedDraft
images?: ImageModel[] images?: ImageModel[]
labels?: string[] labels?: string[]
+6 -4
View File
@@ -1,10 +1,10 @@
import {timeout} from './timeout' import {timeout} from './timeout'
export async function until( export async function until<T>(
retries: number, retries: number,
delay: number, delay: number,
cond: (v: any, err: any) => boolean, cond: (v: T, err: any) => boolean,
fn: () => Promise<any>, fn: () => Promise<T>,
): Promise<boolean> { ): Promise<boolean> {
while (retries > 0) { while (retries > 0) {
try { try {
@@ -13,7 +13,9 @@ export async function until(
return true return true
} }
} catch (e: any) { } 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 return true
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import React from 'react'
export const useDedupe = () => { export const useDedupe = () => {
const canDo = React.useRef(true) const canDo = React.useRef(true)
return React.useRef((cb: () => unknown) => { return React.useCallback((cb: () => unknown) => {
if (canDo.current) { if (canDo.current) {
canDo.current = false canDo.current = false
setTimeout(() => { setTimeout(() => {
@@ -13,5 +13,5 @@ export const useDedupe = () => {
return true return true
} }
return false return false
}).current }, [])
} }
+2 -2
View File
@@ -17,14 +17,14 @@ export async function openPicker(opts?: ImagePickerOptions) {
}) })
if (response.assets && response.assets.length > 4) { 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 ?? []) return (response.assets ?? [])
.slice(0, 4) .slice(0, 4)
.filter(asset => { .filter(asset => {
if (asset.mimeType?.startsWith('image/')) return true 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 return false
}) })
.map(image => ({ .map(image => ({
+36
View File
@@ -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
}
+6 -9
View File
@@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string} MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined MessagesSettings: undefined
NotificationsSettings: undefined
Feeds: undefined Feeds: undefined
Start: {name: string; rkey: string} Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: undefined StarterPackWizard: undefined
StarterPackEdit: { StarterPackEdit: {rkey?: string}
rkey?: string
}
} }
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {
@@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
} }
export type NotificationsTabNavigatorParams = CommonNavigatorParams & { export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined Notifications: {show?: 'all'}
} }
export type MyProfileTabNavigatorParams = CommonNavigatorParams & { export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
@@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
Notifications: undefined Notifications: {show?: 'all'}
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
} }
@@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Search: {q?: string} Search: {q?: string}
Feeds: undefined Feeds: undefined
NotificationsTab: undefined NotificationsTab: undefined
Notifications: undefined Notifications: {show?: 'all'}
MyProfileTab: undefined MyProfileTab: undefined
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesTab: undefined MessagesTab: undefined
@@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
StarterPack: {name: string; rkey: string; new?: boolean} StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackShort: {code: string} StarterPackShort: {code: string}
StarterPackWizard: undefined StarterPackWizard: undefined
StarterPackEdit: { StarterPackEdit: {rkey?: string}
rkey?: string
}
} }
// NOTE // NOTE
+1 -1
View File
@@ -20,6 +20,6 @@ export async function shareUrl(url: string) {
// React Native Share is not supported by web. Web Share API // React Native Share is not supported by web. Web Share API
// has increasing but not full support, so default to clipboard // has increasing but not full support, so default to clipboard
setStringAsync(url) setStringAsync(url)
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard', 'clipboard-check')
} }
} }
+1
View File
@@ -14,3 +14,4 @@ export type Gate =
| 'suggested_follows_interstitial' | 'suggested_follows_interstitial'
| 'ungroup_follow_backs' | 'ungroup_follow_backs'
| 'videos' | 'videos'
| 'small_avi_thumb'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -5,6 +5,7 @@ export const router = new Router({
Search: '/search', Search: '/search',
Feeds: '/feeds', Feeds: '/feeds',
Notifications: '/notifications', Notifications: '/notifications',
NotificationsSettings: '/notifications/settings',
Settings: '/settings', Settings: '/settings',
LanguageSettings: '/settings/language', LanguageSettings: '/settings/language',
Lists: '/lists', Lists: '/lists',
@@ -84,7 +84,7 @@ function DialogInner() {
}, },
onError: err => { onError: err => {
logger.error('Failed to submit chat appeal', {message: 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: () => { onSuccess: () => {
control.close() control.close()
@@ -67,7 +67,7 @@ export function MessageInput({
return return
} }
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { 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 return
} }
clearDraft() clearDraft()
@@ -46,7 +46,7 @@ export function MessageInput({
return return
} }
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { 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 return
} }
clearDraft() clearDraft()
@@ -106,6 +106,7 @@ function Inner() {
title={_(msg`Something went wrong`)} title={_(msg`Something went wrong`)}
message={_(msg`We couldn't load this conversation`)} message={_(msg`We couldn't load this conversation`)}
onRetry={() => convoState.error.retry()} onRetry={() => convoState.error.retry()}
sideBorders={false}
/> />
</CenteredView> </CenteredView>
) )
+1 -1
View File
@@ -309,7 +309,7 @@ function DesktopHeader({
a.gap_lg, a.gap_lg,
a.px_lg, a.px_lg,
a.pr_md, a.pr_md,
a.py_md, a.py_sm,
a.border_b, a.border_b,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
+5 -5
View File
@@ -11,7 +11,7 @@ import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader' 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 {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
@@ -32,7 +32,7 @@ export function MessagesSettingsScreen({}: Props) {
const {mutate: updateDeclaration} = useUpdateActorDeclaration({ const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => { 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 ( return (
<CenteredView sideBorders style={a.h_full_vh}> <ScrollView stickyHeaderIndices={[0]}>
<ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder /> <ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
<View style={[a.p_lg, a.gap_md]}> <View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_bold]}> <Text style={[a.text_lg, a.font_bold]}>
@@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
a.rounded_md, a.rounded_md,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
]}> ]}>
<Text style={[t.atoms.text_contrast_high]}> <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
<Trans> <Trans>
You can continue ongoing conversations regardless of which setting You can continue ongoing conversations regardless of which setting
you choose. you choose.
@@ -149,6 +149,6 @@ export function MessagesSettingsScreen({}: Props) {
</> </>
)} )}
</View> </View>
</CenteredView> </ScrollView>
) )
} }
+16 -4
View File
@@ -240,7 +240,10 @@ export function ModerationScreenInner({
)} )}
</Button> </Button>
<Divider /> <Divider />
<Link testID="moderationlistsBtn" to="/moderation/modlists"> <Link
label={_(msg`View your moderation lists`)}
testID="moderationlistsBtn"
to="/moderation/modlists">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Moderation lists`)} title={_(msg`Moderation lists`)}
@@ -252,7 +255,10 @@ export function ModerationScreenInner({
)} )}
</Link> </Link>
<Divider /> <Divider />
<Link testID="mutedAccountsBtn" to="/moderation/muted-accounts"> <Link
label={_(msg`View your muted accounts`)}
testID="mutedAccountsBtn"
to="/moderation/muted-accounts">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Muted accounts`)} title={_(msg`Muted accounts`)}
@@ -264,7 +270,10 @@ export function ModerationScreenInner({
)} )}
</Link> </Link>
<Divider /> <Divider />
<Link testID="blockedAccountsBtn" to="/moderation/blocked-accounts"> <Link
label={_(msg`View your blocked accounts`)}
testID="blockedAccountsBtn"
to="/moderation/blocked-accounts">
{state => ( {state => (
<SubItem <SubItem
title={_(msg`Blocked accounts`)} title={_(msg`Blocked accounts`)}
@@ -356,6 +365,7 @@ export function ModerationScreenInner({
<Trans> <Trans>
Adult content can only be enabled via the Web at{' '} Adult content can only be enabled via the Web at{' '}
<InlineLinkText <InlineLinkText
label={_(msg`The Bluesky web application`)}
to="" to=""
onPress={evt => { onPress={evt => {
evt.preventDefault() evt.preventDefault()
@@ -569,7 +579,9 @@ function PwiOptOut() {
</Trans> </Trans>
</Text> </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> <Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLinkText> </InlineLinkText>
</View> </View>
@@ -116,6 +116,7 @@ let ProfileHeaderLabeler = ({
_( _(
msg`There was an an issue contacting the server, please check your internet connection and try again.`, 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}) logger.error(`Failed to toggle labeler like`, {message: e.message})
} }
@@ -104,7 +104,7 @@ let ProfileHeaderStandard = ({
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {message: String(e)}) 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) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {message: String(e)}) 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) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {message: e}) 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]) }, [_, queueUnblock, track])
+8 -2
View File
@@ -45,14 +45,20 @@ export const Policies = ({
const els = [] const els = []
if (tos) { if (tos) {
els.push( els.push(
<InlineLinkText key="tos" to={tos}> <InlineLinkText
label={_(msg`Read the Bluesky Terms of Service`)}
key="tos"
to={tos}>
{_(msg`Terms of Service`)} {_(msg`Terms of Service`)}
</InlineLinkText>, </InlineLinkText>,
) )
} }
if (pp) { if (pp) {
els.push( els.push(
<InlineLinkText key="pp" to={pp}> <InlineLinkText
label={_(msg`Read the Bluesky Privacy Policy`)}
key="pp"
to={pp}>
{_(msg`Privacy Policy`)} {_(msg`Privacy Policy`)}
</InlineLinkText>, </InlineLinkText>,
) )
+1
View File
@@ -166,6 +166,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<Text style={[t.atoms.text, !gtMobile && a.text_md]}> <Text style={[t.atoms.text, !gtMobile && a.text_md]}>
<Trans>Having trouble?</Trans>{' '} <Trans>Having trouble?</Trans>{' '}
<InlineLinkText <InlineLinkText
label={_(msg`Contact support`)}
to={FEEDBACK_FORM_URL({email: state.email})} to={FEEDBACK_FORM_URL({email: state.email})}
style={[!gtMobile && a.text_md]}> style={[!gtMobile && a.text_md]}>
<Trans>Contact support</Trans> <Trans>Contact support</Trans>
@@ -359,7 +359,7 @@ function Header({
captureAction(ProgressGuideAction.Follow, dids.length) captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`)) Toast.show(_(msg`All accounts have been followed!`))
} catch (e) { } 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 { } finally {
setIsProcessing(false) setIsProcessing(false)
} }
@@ -672,7 +672,7 @@ function InvalidStarterPack({rkey}: {rkey: string}) {
onError: e => { onError: e => {
setIsProcessing(false) setIsProcessing(false)
logger.error('Failed to delete invalid starter pack', {safeMessage: e}) 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')
}, },
}) })
+5 -2
View File
@@ -74,7 +74,10 @@ function reducer(state: State, action: Action): State {
break break
case 'AddProfile': case 'AddProfile':
if (state.profiles.length >= 51) { 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 { } else {
updatedState = {...state, profiles: [...state.profiles, action.profile]} updatedState = {...state, profiles: [...state.profiles, action.profile]}
} }
@@ -89,7 +92,7 @@ function reducer(state: State, action: Action): State {
break break
case 'AddFeed': case 'AddFeed':
if (state.feeds.length >= 50) { 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 { } else {
updatedState = {...state, feeds: [...state.feeds, action.feed]} updatedState = {...state, feeds: [...state.feeds, action.feed]}
} }
+2 -2
View File
@@ -227,7 +227,7 @@ function WizardInner({
onError: e => { onError: e => {
logger.error('Failed to create starter pack', {safeMessage: e}) logger.error('Failed to create starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false}) 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({ const {mutate: editStarterPack} = useEditStarterPackMutation({
@@ -235,7 +235,7 @@ function WizardInner({
onError: e => { onError: e => {
logger.error('Failed to edit starter pack', {safeMessage: e}) logger.error('Failed to edit starter pack', {safeMessage: e})
dispatch({type: 'SetProcessing', processing: false}) dispatch({type: 'SetProcessing', processing: false})
Toast.show(_(msg`Failed to create starter pack`)) Toast.show(_(msg`Failed to create starter pack`), 'xmark')
}, },
}) })
+22 -11
View File
@@ -45,9 +45,13 @@ export function useActorAutocompleteQuery(
}, },
select: React.useCallback( select: React.useCallback(
(data: AppBskyActorDefs.ProfileViewBasic[]) => { (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, placeholderData: maintainData ? keepPreviousData : undefined,
}) })
@@ -81,19 +85,25 @@ export function useActorAutocompleteFn() {
} }
} }
return computeSuggestions( return computeSuggestions({
res?.data.actors, q: query,
moderationOpts || DEFAULT_MOD_OPTS, searched: res?.data.actors,
) moderationOpts: moderationOpts || DEFAULT_MOD_OPTS,
})
}, },
[queryClient, moderationOpts, agent], [queryClient, moderationOpts, agent],
) )
} }
function computeSuggestions( function computeSuggestions({
searched: AppBskyActorDefs.ProfileViewBasic[] = [], q,
moderationOpts: ModerationOpts, searched = [],
) { moderationOpts,
}: {
q?: string
searched?: AppBskyActorDefs.ProfileViewBasic[]
moderationOpts: ModerationOpts
}) {
let items: AppBskyActorDefs.ProfileViewBasic[] = [] let items: AppBskyActorDefs.ProfileViewBasic[] = []
for (const item of searched) { for (const item of searched) {
if (!items.find(item2 => item2.handle === item.handle)) { if (!items.find(item2 => item2.handle === item.handle)) {
@@ -102,6 +112,7 @@ function computeSuggestions(
} }
return items.filter(profile => { return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList') 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)
}) })
} }
+22 -15
View File
@@ -46,11 +46,14 @@ const PAGE_SIZE = 30
type RQPageParam = string | undefined type RQPageParam = string | undefined
const RQKEY_ROOT = 'notification-feed' const RQKEY_ROOT = 'notification-feed'
export function RQKEY() { export function RQKEY(priority?: false) {
return [RQKEY_ROOT] return [RQKEY_ROOT, priority]
} }
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { export function useNotificationFeedQuery(opts?: {
enabled?: boolean
overridePriorityNotifications?: boolean
}) {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
const lastPageCountRef = useRef(0) const lastPageCountRef = useRef(0)
const gate = useGate() const gate = useGate()
// false: force showing all notifications
// undefined: let the server decide
const priority = opts?.overridePriorityNotifications ? false : undefined
const query = useInfiniteQuery< const query = useInfiniteQuery<
FeedPage, FeedPage,
Error, Error,
@@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
RQPageParam RQPageParam
>({ >({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
queryKey: RQKEY(), queryKey: RQKEY(priority),
async queryFn({pageParam}: {pageParam: RQPageParam}) { async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page let page
if (!pageParam) { if (!pageParam) {
@@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
page = unreads.getCachedUnreadPage() page = unreads.getCachedUnreadPage()
} }
if (!page) { if (!page) {
page = ( const {page: fetchedPage} = await fetchPage({
await fetchPage({ agent,
agent, limit: PAGE_SIZE,
limit: PAGE_SIZE, cursor: pageParam,
cursor: pageParam, queryClient,
queryClient, moderationOpts,
moderationOpts, fetchAdditionalData: true,
fetchAdditionalData: true, shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), priority,
}) })
).page page = fetchedPage
} }
// if the first page has an unread, mark all read // 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,
}
}),
}
})
}
+1
View File
@@ -22,6 +22,7 @@ export interface FeedPage {
cursor: string | undefined cursor: string | undefined
seenAt: Date seenAt: Date
items: FeedNotification[] items: FeedNotification[]
priority: boolean
} }
export interface CachedFeedPage { export interface CachedFeedPage {
+7 -1
View File
@@ -39,10 +39,15 @@ export async function fetchPage({
moderationOpts: ModerationOpts | undefined moderationOpts: ModerationOpts | undefined
fetchAdditionalData: boolean fetchAdditionalData: boolean
shouldUngroupFollowBacks?: () => boolean shouldUngroupFollowBacks?: () => boolean
}): Promise<{page: FeedPage; indexedAt: string | undefined}> { priority?: boolean
}): Promise<{
page: FeedPage
indexedAt: string | undefined
}> {
const res = await agent.listNotifications({ const res = await agent.listNotifications({
limit, limit,
cursor, cursor,
// priority,
}) })
const indexedAt = res.data.notifications[0]?.indexedAt const indexedAt = res.data.notifications[0]?.indexedAt
@@ -88,6 +93,7 @@ export async function fetchPage({
cursor: res.data.cursor, cursor: res.data.cursor,
seenAt, seenAt,
items: notifsGrouped, items: notifsGrouped,
priority: res.data.priority ?? false,
}, },
indexedAt, indexedAt,
} }
+31 -36
View File
@@ -31,7 +31,7 @@ import {LikesFeedAPI} from 'lib/api/feed/likes'
import {ListFeedAPI} from 'lib/api/feed/list' import {ListFeedAPI} from 'lib/api/feed/list'
import {MergeFeedAPI} from 'lib/api/feed/merge' import {MergeFeedAPI} from 'lib/api/feed/merge'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types' import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip' import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip'
import {BSKY_FEED_OWNER_DIDS} from 'lib/constants' import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
import {KnownError} from '#/view/com/posts/FeedErrorMessage' import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners' import {useFeedTuners} from '../preferences/feed-tuners'
@@ -61,7 +61,6 @@ export type FeedDescriptor =
| `list|${ListUri}` | `list|${ListUri}`
| `list|${ListUri}|${ListFilter}` | `list|${ListUri}|${ListFilter}`
export interface FeedParams { export interface FeedParams {
disableTuner?: boolean
mergeFeedEnabled?: boolean mergeFeedEnabled?: boolean
mergeFeedSources?: string[] mergeFeedSources?: string[]
} }
@@ -78,7 +77,10 @@ export interface FeedPostSliceItem {
uri: string uri: string
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record: AppBskyFeedPost.Record record: AppBskyFeedPost.Record
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource reason?:
| AppBskyFeedDefs.ReasonRepost
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
feedContext: string | undefined feedContext: string | undefined
moderation: ModerationDecision moderation: ModerationDecision
parentAuthor?: AppBskyActorDefs.ProfileViewBasic parentAuthor?: AppBskyActorDefs.ProfileViewBasic
@@ -102,7 +104,7 @@ export interface FeedPageUnselected {
export interface FeedPage { export interface FeedPage {
api: FeedAPI api: FeedAPI
tuner: FeedTuner | NoopFeedTuner tuner: FeedTuner
cursor: string | undefined cursor: string | undefined
slices: FeedPostSlice[] slices: FeedPostSlice[]
fetchedAt: number fetchedAt: number
@@ -139,18 +141,11 @@ export function usePostFeedQuery(
const selectArgs = React.useMemo( const selectArgs = React.useMemo(
() => ({ () => ({
feedTuners, feedTuners,
disableTuner: params?.disableTuner,
moderationOpts, moderationOpts,
ignoreFilterFor: opts?.ignoreFilterFor, ignoreFilterFor: opts?.ignoreFilterFor,
isDiscover, isDiscover,
}), }),
[ [feedTuners, moderationOpts, opts?.ignoreFilterFor, isDiscover],
feedTuners,
params?.disableTuner,
moderationOpts,
opts?.ignoreFilterFor,
isDiscover,
],
) )
const query = useInfiniteQuery< const query = useInfiniteQuery<
@@ -229,17 +224,10 @@ export function usePostFeedQuery(
(data: InfiniteData<FeedPageUnselected, RQPageParam>) => { (data: InfiniteData<FeedPageUnselected, RQPageParam>) => {
// If the selection depends on some data, that data should // If the selection depends on some data, that data should
// be included in the selectArgs object and read here. // be included in the selectArgs object and read here.
const { const {feedTuners, moderationOpts, ignoreFilterFor, isDiscover} =
feedTuners, selectArgs
disableTuner,
moderationOpts,
ignoreFilterFor,
isDiscover,
} = selectArgs
const tuner = disableTuner const tuner = new FeedTuner(feedTuners)
? new NoopFeedTuner()
: new FeedTuner(feedTuners)
// Keep track of the last run and whether we can reuse // Keep track of the last run and whether we can reuse
// some already selected pages from there. // some already selected pages from there.
@@ -311,7 +299,7 @@ export function usePostFeedQuery(
if (isDiscover) { if (isDiscover) {
userActionHistory.seen( userActionHistory.seen(
slice.items.map(item => ({ slice.items.map(item => ({
feedContext: item.feedContext, feedContext: slice.feedContext,
likeCount: item.post.likeCount ?? 0, likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0, repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0, replyCount: item.post.replyCount ?? 0,
@@ -323,10 +311,10 @@ export function usePostFeedQuery(
) )
} }
return { const feedPostSlice: FeedPostSlice = {
_reactKey: slice._reactKey, _reactKey: slice._reactKey,
_isFeedPostSlice: true, _isFeedPostSlice: true,
rootUri: slice.rootItem.post.uri, rootUri: slice.uri,
isThread: isThread:
slice.items.length > 1 && slice.items.length > 1 &&
slice.items.every( slice.items.every(
@@ -341,35 +329,42 @@ export function usePostFeedQuery(
AppBskyFeedPost.validateRecord(item.post.record) AppBskyFeedPost.validateRecord(item.post.record)
.success .success
) { ) {
const parentAuthor = const parent = item.reply?.parent
item.reply?.parent?.author ?? let parentAuthor:
slice.items[i + 1]?.reply?.grandparentAuthor | AppBskyActorDefs.ProfileViewBasic
| undefined
if (AppBskyFeedDefs.isPostView(parent)) {
parentAuthor = parent.author
}
if (!parentAuthor) {
parentAuthor =
slice.items[i + 1]?.reply?.grandparentAuthor
}
const replyRef = item.reply const replyRef = item.reply
const isParentBlocked = AppBskyFeedDefs.isBlockedPost( const isParentBlocked = AppBskyFeedDefs.isBlockedPost(
replyRef?.parent, replyRef?.parent,
) )
return { const feedPostSliceItem: FeedPostSliceItem = {
_reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.post.record, record: item.post.record,
reason: reason: slice.reason,
i === 0 && slice.source feedContext: slice.feedContext,
? slice.source
: item.reason,
feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i], moderation: moderations[i],
parentAuthor, parentAuthor,
isParentBlocked, isParentBlocked,
} }
return feedPostSliceItem
} }
return undefined return undefined
}) })
.filter(Boolean) as FeedPostSliceItem[], .filter(n => !!n),
} }
return feedPostSlice
}) })
.filter(Boolean) as FeedPostSlice[], .filter(n => !!n),
})), })),
], ],
} }
+11 -11
View File
@@ -4,7 +4,7 @@ export type ThreadgateSetting =
| {type: 'nobody'} | {type: 'nobody'}
| {type: 'mention'} | {type: 'mention'}
| {type: 'following'} | {type: 'following'}
| {type: 'list'; list: string} | {type: 'list'; list: unknown}
export function threadgateViewToSettings( export function threadgateViewToSettings(
threadgate: AppBskyFeedDefs.ThreadgateView | undefined, threadgate: AppBskyFeedDefs.ThreadgateView | undefined,
@@ -21,18 +21,18 @@ export function threadgateViewToSettings(
if (!record.allow?.length) { if (!record.allow?.length) {
return [{type: 'nobody'}] return [{type: 'nobody'}]
} }
return record.allow const settings: ThreadgateSetting[] = record.allow
.map(allow => { .map(allow => {
let setting: ThreadgateSetting | undefined
if (allow.$type === 'app.bsky.feed.threadgate#mentionRule') { 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 setting
return {type: 'following'}
}
if (allow.$type === 'app.bsky.feed.threadgate#listRule') {
return {type: 'list', list: allow.list}
}
return undefined
}) })
.filter(Boolean) as ThreadgateSetting[] .filter(n => !!n)
return settings
} }
+31
View File
@@ -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
}
+15
View File
@@ -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
}
+59
View File
@@ -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,
})
}
+212
View File
@@ -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)
},
}
}
+18
View File
@@ -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)
}
+10 -3
View File
@@ -132,6 +132,7 @@ export const SplashScreen = ({
function Footer() { function Footer() {
const t = useTheme() const t = useTheme()
const {_} = useLingui()
return ( return (
<View <View
@@ -147,13 +148,19 @@ function Footer() {
a.flex_1, a.flex_1,
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
]}> ]}>
<InlineLinkText to="https://bsky.social"> <InlineLinkText
label={_(msg`Learn more about Bluesky`)}
to="https://bsky.social">
<Trans>Business</Trans> <Trans>Business</Trans>
</InlineLinkText> </InlineLinkText>
<InlineLinkText to="https://bsky.social/about/blog"> <InlineLinkText
label={_(msg`Read the Bluesky blog`)}
to="https://bsky.social/about/blog">
<Trans>Blog</Trans> <Trans>Blog</Trans>
</InlineLinkText> </InlineLinkText>
<InlineLinkText to="https://bsky.social/about/join"> <InlineLinkText
label={_(msg`See jobs at Bluesky`)}
to="https://bsky.social/about/join">
<Trans>Jobs</Trans> <Trans>Jobs</Trans>
</InlineLinkText> </InlineLinkText>
+140 -62
View File
@@ -13,10 +13,16 @@ import {
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
LayoutChangeEvent, LayoutChangeEvent,
StyleProp,
StyleSheet, StyleSheet,
View, View,
ViewStyle,
} from 'react-native' } from 'react-native'
// @ts-expect-error no type definition
import ProgressCircle from 'react-native-progress/Circle'
import Animated, { import Animated, {
FadeIn,
FadeOut,
interpolateColor, interpolateColor,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
@@ -55,6 +61,7 @@ import {
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {Gif} from '#/state/queries/tenor' import {Gif} from '#/state/queries/tenor'
import {ThreadgateSetting} from '#/state/queries/threadgate' import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useUploadVideo} from '#/state/queries/video/video'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics' 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 {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
import {useDialogStateControlContext} from 'state/dialogs' import {useDialogStateControlContext} from 'state/dialogs'
import {GalleryModel} from 'state/models/media/gallery' import {GalleryModel} from 'state/models/media/gallery'
import {State as VideoUploadState} from 'state/queries/video/video'
import {ComposerOpts} from 'state/shell/composer' import {ComposerOpts} from 'state/shell/composer'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -96,10 +104,8 @@ import {TextInput, TextInputRef} from './text-input/TextInput'
import {ThreadgateBtn} from './threadgate/ThreadgateBtn' import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
import {useExternalLinkFetch} from './useExternalLinkFetch' import {useExternalLinkFetch} from './useExternalLinkFetch'
import {SelectVideoBtn} from './videos/SelectVideoBtn' import {SelectVideoBtn} from './videos/SelectVideoBtn'
import {useVideoState} from './videos/state'
import {VideoPreview} from './videos/VideoPreview' import {VideoPreview} from './videos/VideoPreview'
import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress'
import hairlineWidth = StyleSheet.hairlineWidth
type CancelRef = { type CancelRef = {
onPressCancel: () => void onPressCancel: () => void
@@ -160,14 +166,21 @@ export const ComposePost = observer(function ComposePost({
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>( const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
initQuote, initQuote,
) )
const { const {
video, selectVideo,
onSelectVideo,
videoPending,
videoProcessingData,
clearVideo, clearVideo,
videoProcessingProgress, state: videoUploadState,
} = useVideoState({setError}) } = useUploadVideo({
setStatus: (status: string) => setProcessingState(status),
onSuccess: () => {
if (publishOnUpload) {
onPressPublish(true)
}
},
})
const [publishOnUpload, setPublishOnUpload] = useState(false)
const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
const [extGif, setExtGif] = useState<Gif>() const [extGif, setExtGif] = useState<Gif>()
const [labels, setLabels] = useState<string[]>([]) const [labels, setLabels] = useState<string[]>([])
@@ -275,7 +288,7 @@ export const ComposePost = observer(function ComposePost({
return false return false
}, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled]) }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
const onPressPublish = async () => { const onPressPublish = async (finishedUploading?: boolean) => {
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
return return
} }
@@ -284,6 +297,15 @@ export const ComposePost = observer(function ComposePost({
return return
} }
if (
!finishedUploading &&
videoUploadState.status !== 'idle' &&
videoUploadState.asset
) {
setPublishOnUpload(true)
return
}
setError('') setError('')
if ( if (
@@ -388,8 +410,12 @@ export const ComposePost = observer(function ComposePost({
: _(msg`What's up?`) : _(msg`What's up?`)
const canSelectImages = const canSelectImages =
gallery.size < 4 && !extLink && !video && !videoPending gallery.size < 4 &&
const hasMedia = gallery.size > 0 || Boolean(extLink) || Boolean(video) !extLink &&
videoUploadState.status === 'idle' &&
!videoUploadState.video
const hasMedia =
gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
const onEmojiButtonPress = useCallback(() => { const onEmojiButtonPress = useCallback(() => {
openPicker?.(textInput.current?.getCursorPosition()) openPicker?.(textInput.current?.getCursorPosition())
@@ -501,7 +527,10 @@ export const ComposePost = observer(function ComposePost({
shape="default" shape="default"
size="small" size="small"
style={[a.rounded_full, a.py_sm]} style={[a.rounded_full, a.py_sm]}
onPress={onPressPublish}> onPress={() => onPressPublish()}
disabled={
videoUploadState.status !== 'idle' && publishOnUpload
}>
<ButtonText style={[a.text_md]}> <ButtonText style={[a.text_md]}>
{replyTo ? ( {replyTo ? (
<Trans context="action">Reply</Trans> <Trans context="action">Reply</Trans>
@@ -573,7 +602,7 @@ export const ComposePost = observer(function ComposePost({
autoFocus autoFocus
setRichText={setRichText} setRichText={setRichText}
onPhotoPasted={onPhotoPasted} onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish} onPressPublish={() => onPressPublish()}
onNewLink={onNewLink} onNewLink={onNewLink}
onError={setError} onError={setError}
accessible={true} accessible={true}
@@ -603,29 +632,33 @@ export const ComposePost = observer(function ComposePost({
</View> </View>
)} )}
{quote ? ( <View style={[a.mt_md]}>
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}> {quote ? (
<View style={{pointerEvents: 'none'}}> <View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
<QuoteEmbed quote={quote} /> <View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
{quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View> </View>
{quote.uri !== initQuote?.uri && ( ) : null}
<QuoteX onRemove={() => setQuote(undefined)} /> {videoUploadState.status === 'compressing' &&
)} videoUploadState.asset ? (
</View> <VideoTranscodeProgress
) : null} asset={videoUploadState.asset}
{videoPending && videoProcessingData ? ( progress={videoUploadState.progress}
<VideoTranscodeProgress />
input={videoProcessingData} ) : videoUploadState.video ? (
progress={videoProcessingProgress}
/>
) : (
video && (
// remove suspense when we get rid of lazy // remove suspense when we get rid of lazy
<Suspense fallback={null}> <Suspense fallback={null}>
<VideoPreview video={video} clear={clearVideo} /> <VideoPreview
video={videoUploadState.video}
clear={clearVideo}
/>
</Suspense> </Suspense>
) ) : null}
)} </View>
</Animated.ScrollView> </Animated.ScrollView>
<SuggestedLanguage text={richtext.text} /> <SuggestedLanguage text={richtext.text} />
@@ -642,33 +675,37 @@ export const ComposePost = observer(function ComposePost({
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
styles.bottomBar, styles.bottomBar,
]}> ]}>
<View style={[a.flex_row, a.align_center, a.gap_xs]}> {videoUploadState.status !== 'idle' ? (
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} /> <VideoUploadToolbar state={videoUploadState} />
{gate('videos') && ( ) : (
<SelectVideoBtn <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
onSelectVideo={onSelectVideo} <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
disabled={!canSelectImages} {gate('videos') && (
<SelectVideoBtn
onSelectVideo={selectVideo}
disabled={!canSelectImages}
/>
)}
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
<SelectGifBtn
onClose={focusTextInput}
onSelectGif={onSelectGif}
disabled={hasMedia}
/> />
)} {!isMobile ? (
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> <Button
<SelectGifBtn onPress={onEmojiButtonPress}
onClose={focusTextInput} style={a.p_sm}
onSelectGif={onSelectGif} label={_(msg`Open emoji picker`)}
disabled={hasMedia} accessibilityHint={_(msg`Open emoji picker`)}
/> variant="ghost"
{!isMobile ? ( shape="round"
<Button color="primary">
onPress={onEmojiButtonPress} <EmojiSmile size="lg" />
style={a.p_sm} </Button>
label={_(msg`Open emoji picker`)} ) : null}
accessibilityHint={_(msg`Open emoji picker`)} </ToolbarWrapper>
variant="ghost" )}
shape="round"
color="primary">
<EmojiSmile size="lg" />
</Button>
) : null}
</View>
<View style={a.flex_1} /> <View style={a.flex_1} />
<SelectLangBtn /> <SelectLangBtn />
<CharProgress count={graphemeLength} /> <CharProgress count={graphemeLength} />
@@ -763,7 +800,7 @@ function useAnimatedBorders() {
const topBarAnimatedStyle = useAnimatedStyle(() => { const topBarAnimatedStyle = useAnimatedStyle(() => {
return { return {
borderBottomWidth: hairlineWidth, borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledTop.value, hasScrolledTop.value,
[0, 1], [0, 1],
@@ -773,7 +810,7 @@ function useAnimatedBorders() {
}) })
const bottomBarAnimatedStyle = useAnimatedStyle(() => { const bottomBarAnimatedStyle = useAnimatedStyle(() => {
return { return {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderColor: interpolateColor( borderColor: interpolateColor(
hasScrolledBottom.value, hasScrolledBottom.value,
[0, 1], [0, 1],
@@ -855,7 +892,7 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
errorIcon: { errorIcon: {
borderWidth: hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.red4, borderColor: colors.red4,
color: colors.red4, color: colors.red4,
borderRadius: 30, borderRadius: 30,
@@ -891,6 +928,47 @@ const styles = StyleSheet.create({
paddingLeft: 7, paddingLeft: 7,
paddingRight: 16, paddingRight: 16,
alignItems: 'center', 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>
)
}
+1 -2
View File
@@ -9,7 +9,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) { export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -49,7 +48,7 @@ const styles = StyleSheet.create({
paddingBottom: 10, paddingBottom: 10,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
labelMobile: { labelMobile: {
paddingLeft: 12, paddingLeft: 12,
@@ -17,6 +17,7 @@ export function VideoPreview({
const player = useVideoPlayer(video.uri, player => { const player = useVideoPlayer(video.uri, player => {
player.loop = true player.loop = true
player.play() player.play()
player.volume = 0
}) })
return ( return (
@@ -9,15 +9,15 @@ import {Text} from '#/components/Typography'
import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
export function VideoTranscodeProgress({ export function VideoTranscodeProgress({
input, asset,
progress, progress,
}: { }: {
input: ImagePickerAsset asset: ImagePickerAsset
progress: number progress: number
}) { }) {
const t = useTheme() const t = useTheme()
const aspectRatio = input.width / input.height const aspectRatio = asset.width / asset.height
return ( return (
<View <View
@@ -29,7 +29,7 @@ export function VideoTranscodeProgress({
a.overflow_hidden, a.overflow_hidden,
{aspectRatio: isNaN(aspectRatio) ? 16 / 9 : aspectRatio}, {aspectRatio: isNaN(aspectRatio) ? 16 / 9 : aspectRatio},
]}> ]}>
<VideoTranscodeBackdrop uri={input.uri} /> <VideoTranscodeBackdrop uri={asset.uri} />
<View <View
style={[ style={[
a.flex_1, a.flex_1,
-51
View File
@@ -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
}
+5 -6
View File
@@ -13,6 +13,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger' import {logger} from '#/logger'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import { import {
useAddSavedFeedsMutation, useAddSavedFeedsMutation,
@@ -32,8 +33,6 @@ import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import hairlineWidth = StyleSheet.hairlineWidth
import {shouldClickOpenNewTab} from '#/platform/urls'
export function FeedSourceCard({ export function FeedSourceCard({
feedUri, feedUri,
@@ -125,7 +124,7 @@ export function FeedSourceCardLoaded({
]) ])
Toast.show(_(msg`Added to my feeds`)) Toast.show(_(msg`Added to my feeds`))
} catch (e) { } 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}) logger.error('Failed to save feed', {message: e})
} }
}, [_, feed, pinOnSave, addSavedFeeds, isSaved]) }, [_, feed, pinOnSave, addSavedFeeds, isSaved])
@@ -138,7 +137,7 @@ export function FeedSourceCardLoaded({
// await item.unsave() // await item.unsave()
Toast.show(_(msg`Removed from my feeds`)) Toast.show(_(msg`Removed from my feeds`))
} catch (e) { } 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}) logger.error('Failed to unsave feed', {message: e})
} }
}, [_, removeFeed, savedFeedConfig]) }, [_, removeFeed, savedFeedConfig])
@@ -209,7 +208,7 @@ export function FeedSourceCardLoaded({
styles.container, styles.container,
pal.border, pal.border,
style, style,
{borderTopWidth: hideTopBorder ? 0 : hairlineWidth}, {borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
]} ]}
onPress={e => { onPress={e => {
const shouldOpenInNewTab = shouldClickOpenNewTab(e) const shouldOpenInNewTab = shouldClickOpenNewTab(e)
@@ -330,7 +329,7 @@ const styles = StyleSheet.create({
gap: 14, gap: 14,
}, },
border: { border: {
borderTopWidth: hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
}, },
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
+1 -1
View File
@@ -73,7 +73,7 @@ export function HomeHeaderLayoutMobile({
]}> ]}>
{IS_DEV && ( {IS_DEV && (
<> <>
<Link to="/sys/debug"> <Link label="View storybook" to="/sys/debug">
<ColorPalette size="md" /> <ColorPalette size="md" />
</Link> </Link>
</> </>

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