diff --git a/app.config.js b/app.config.js index 25014ee8f1..ddd72f75f0 100644 --- a/app.config.js +++ b/app.config.js @@ -211,7 +211,6 @@ module.exports = function (config) { sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], }, ], - 'expo-video', 'react-native-compressor', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/withAndroidManifestPlugin.js', diff --git a/assets/icons/download_stroke2_corner0_rounded.svg b/assets/icons/download_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..899fef3e5e --- /dev/null +++ b/assets/icons/download_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index 92b6cfe151..1cff0d4534 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.91.0", + "version": "1.91.1", "private": true, "engines": { "node": ">=18" @@ -68,6 +68,7 @@ "@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-native-fontawesome": "^0.3.2", + "@haileyok/bluesky-video": "0.1.4", "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.7.1", "@miblanchard/react-native-slider": "^2.3.1", @@ -139,7 +140,6 @@ "expo-system-ui": "~3.0.4", "expo-task-manager": "~11.8.1", "expo-updates": "~0.25.14", - "expo-video": "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz", "expo-web-browser": "~13.0.3", "fast-text-encoding": "^1.0.6", "history": "^5.3.0", diff --git a/src/App.native.tsx b/src/App.native.tsx index 780d4058f9..2ec666e2cc 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -52,17 +52,17 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {TestCtrls} from '#/view/com/testing/TestCtrls' -import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext' +import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {Shell} from '#/view/shell' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' +import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army' SplashScreen.preventAutoHideAsync() @@ -109,8 +109,8 @@ function InnerApp() { - - + + @@ -131,6 +131,7 @@ function InnerApp() { style={s.h100pct}> + @@ -145,8 +146,8 @@ function InnerApp() { - - + + @@ -157,8 +158,6 @@ function App() { const [isReady, setReady] = useState(false) React.useEffect(() => { - PlatformInfo.setAudioCategory(AudioCategory.Ambient) - PlatformInfo.setAudioActive(false) initPersistedState().then(() => setReady(true)) }, []) diff --git a/src/App.web.tsx b/src/App.web.tsx index e6b91c53aa..6efe7cc022 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -42,11 +42,13 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' +import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' import {ToastContainer} from '#/view/com/util/Toast.web' import {Shell} from '#/view/shell/index' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' +import {NuxDialogs} from '#/components/dialogs/nuxs' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as PortalProvider} from '#/components/Portal' @@ -97,41 +99,44 @@ function InnerApp() { - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alf/index.tsx b/src/alf/index.tsx index 5fa7d3b1a1..d699de6a5b 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -18,9 +18,17 @@ export * from '#/alf/util/themeSelector' export const Context = React.createContext<{ themeName: ThemeName theme: Theme + themes: ReturnType }>({ themeName: 'light', theme: defaultTheme, + themes: createThemes({ + hues: { + primary: BLUE_HUE, + negative: RED_HUE, + positive: GREEN_HUE, + }, + }), }) export function ThemeProvider({ @@ -42,18 +50,22 @@ export function ThemeProvider({ ({ + themes, themeName: themeName, theme: theme, }), - [theme, themeName], + [theme, themeName, themes], )}> {children} ) } -export function useTheme() { - return React.useContext(Context).theme +export function useTheme(theme?: ThemeName) { + const ctx = React.useContext(Context) + return React.useMemo(() => { + return theme ? ctx.themes[theme] : ctx.theme + }, [theme, ctx]) } export function useBreakpoints() { diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 158244c8e9..cdce3765f0 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -256,7 +256,7 @@ export const ScrollableInner = React.forwardRef< borderTopLeftRadius: 40, borderTopRightRadius: 40, }, - flatten(style), + style, ]} contentContainerStyle={a.pb_4xl} ref={ref}> diff --git a/src/components/Fill.tsx b/src/components/Fill.tsx new file mode 100644 index 0000000000..ac74f1660f --- /dev/null +++ b/src/components/Fill.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, ViewStyleProp} from '#/alf' + +export function Fill({ + children, + style, +}: {children?: React.ReactNode} & ViewStyleProp) { + return {children} +} diff --git a/src/components/MediaInsetBorder.tsx b/src/components/MediaInsetBorder.tsx new file mode 100644 index 0000000000..ef8b00e2e0 --- /dev/null +++ b/src/components/MediaInsetBorder.tsx @@ -0,0 +1,45 @@ +import React from 'react' + +import {atoms as a, useTheme, ViewStyleProp} from '#/alf' +import {Fill} from '#/components/Fill' + +/** + * Applies and thin border within a bounding box. Used to contrast media from + * bg of the container. + */ +export function MediaInsetBorder({ + children, + style, + opaque, +}: { + children?: React.ReactNode + /** + * Used where this border needs to match adjacent borders, such as in + * external link previews + */ + opaque?: boolean +} & ViewStyleProp) { + const t = useTheme() + const isLight = t.name === 'light' + return ( + + {children} + + ) +} diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx index f2ebb4584e..28609c6f49 100644 --- a/src/components/MediaPreview.tsx +++ b/src/components/MediaPreview.tsx @@ -11,6 +11,7 @@ import {Trans} from '@lingui/macro' import {parseTenorGif} from '#/lib/strings/embed-player' import {atoms as a, useTheme} from '#/alf' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' @@ -104,6 +105,7 @@ export function ImageItem({ accessibilityHint={alt} accessibilityLabel="" /> + {children} ) diff --git a/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx new file mode 100644 index 0000000000..9c8d47afd0 --- /dev/null +++ b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function OnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx new file mode 100644 index 0000000000..1f9467e442 --- /dev/null +++ b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function PointOnePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx new file mode 100644 index 0000000000..4197be8357 --- /dev/null +++ b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TenPercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx new file mode 100644 index 0000000000..0d37971410 --- /dev/null +++ b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +export function TwentyFivePercent({fill}: {fill?: string}) { + return ( + + + + ) +} diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx new file mode 100644 index 0000000000..8d5511fdf0 --- /dev/null +++ b/src/components/dialogs/nuxs/TenMillion/index.tsx @@ -0,0 +1,655 @@ +import React from 'react' +import {View} from 'react-native' +import Animated, {FadeIn} from 'react-native-reanimated' +import ViewShot from 'react-native-view-shot' +import {Image} from 'expo-image' +import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker' +import * as MediaLibrary from 'expo-media-library' +import {moderateProfile} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {networkRetry} from '#/lib/async/retry' +import {getCanvas} from '#/lib/canvas' +import {shareUrl} from '#/lib/sharing' +import {logEvent} from '#/lib/statsig/statsig' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {sanitizeHandle} from '#/lib/strings/handles' +import {isIOS, isNative} from '#/platform/detection' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery} from '#/state/queries/profile' +import {useAgent, useSession} from '#/state/session' +import {useComposerControls} from 'state/shell' +import {formatCount} from '#/view/com/util/numeric/format' +import {Logomark} from '#/view/icons/Logomark' +import * as Toast from 'view/com/util/Toast' +import { + atoms as a, + ThemeProvider, + tokens, + useBreakpoints, + useTheme, +} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' +import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent' +import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent' +import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent' +import {Divider} from '#/components/Divider' +import {GradientFill} from '#/components/GradientFill' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download' +import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +const DEBUG = false +const RATIO = 8 / 10 +const WIDTH = 2000 +const HEIGHT = WIDTH * RATIO + +function getFontSize(count: number) { + const length = count.toString().length + if (length < 7) { + return 80 + } else if (length < 5) { + return 100 + } else { + return 70 + } +} + +function getPercentBadge(percent: number) { + if (percent <= 0.001) { + return PointOnePercent + } else if (percent <= 0.01) { + return OnePercent + } else if (percent <= 0.1) { + return TenPercent + } + return null +} + +function Frame({children}: {children: React.ReactNode}) { + return ( + + {children} + + ) +} + +export function TenMillion() { + const agent = useAgent() + const nuxDialogs = useNuxDialogContext() + const [userNumber, setUserNumber] = React.useState(0) + const fetching = React.useRef(false) + + React.useEffect(() => { + async function fetchUserNumber() { + const isBlueskyHosted = agent.sessionManager.pdsUrl + ?.toString() + .includes('bsky.network') + + if (isBlueskyHosted && agent.session?.accessJwt) { + const res = await fetch( + `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`, + { + headers: { + Authorization: `Bearer ${agent.session.accessJwt}`, + }, + }, + ) + + if (!res.ok) { + throw new Error('Network request failed') + } + + const data = await res.json() + + if (data.number) { + setUserNumber(data.number) + } else { + // should be rare + nuxDialogs.dismissActiveNux() + } + } + } + + if (!fetching.current) { + fetching.current = true + networkRetry(3, fetchUserNumber).catch(() => { + nuxDialogs.dismissActiveNux() + }) + } + }, [ + agent.sessionManager.pdsUrl, + agent.session?.accessJwt, + setUserNumber, + nuxDialogs.dismissActiveNux, + nuxDialogs, + ]) + + return userNumber ? : null +} + +export function TenMillionInner({userNumber}: {userNumber: number}) { + const t = useTheme() + const lightTheme = useTheme('light') + const {_, i18n} = useLingui() + const control = Dialog.useDialogControl() + const {gtMobile} = useBreakpoints() + const {openComposer} = useComposerControls() + const {currentAccount} = useSession() + const { + isLoading: isProfileLoading, + data: profile, + error: profileError, + } = useProfileQuery({ + did: currentAccount!.did, + }) + const moderationOpts = useModerationOpts() + const nuxDialogs = useNuxDialogContext() + const moderation = React.useMemo(() => { + return profile && moderationOpts + ? moderateProfile(profile, moderationOpts) + : undefined + }, [profile, moderationOpts]) + const [uri, setUri] = React.useState(null) + const percent = userNumber / 10_000_000 + const Badge = getPercentBadge(percent) + const isLoadingData = isProfileLoading || !moderation || !profile + const isLoadingImage = !uri + + const error: string = React.useMemo(() => { + if (profileError) { + return _( + msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🩋`, + ) + } + return '' + }, [_, profileError]) + + /* + * Opening and closing + */ + React.useEffect(() => { + const timeout = setTimeout(() => { + control.open() + }, 3e3) + return () => { + clearTimeout(timeout) + } + }, [control]) + const onClose = React.useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + /* + * Actions + */ + const sharePost = React.useCallback(() => { + if (uri) { + control.close(() => { + setTimeout(() => { + logEvent('tmd:post', {}) + openComposer({ + text: _( + msg`Bluesky now has over 10 million users, and I was #${i18n.number( + userNumber, + )}!`, + ), // TODO + imageUris: [ + { + uri, + width: WIDTH, + height: HEIGHT, + }, + ], + }) + }, 1e3) + }) + } + }, [_, i18n, control, openComposer, uri, userNumber]) + const onNativeShare = React.useCallback(() => { + if (uri) { + control.close(() => { + logEvent('tmd:share', {}) + shareUrl(uri) + }) + } + }, [uri, control]) + const onNativeDownload = React.useCallback(async () => { + if (uri) { + const res = await requestMediaLibraryPermissionsAsync() + + if (!res) { + Toast.show( + _( + msg`You must grant access to your photo library to save the image.`, + ), + 'xmark', + ) + return + } + + try { + await MediaLibrary.createAssetAsync(uri) + logEvent('tmd:download', {}) + Toast.show(_(msg`Image saved to your camera roll!`)) + } catch (e: unknown) { + console.log(e) + Toast.show(_(msg`An error occurred while saving the image!`), 'xmark') + return + } + } + }, [_, uri]) + const onWebDownload = React.useCallback(async () => { + if (uri) { + const canvas = await getCanvas(uri) + const imgHref = canvas + .toDataURL('image/png') + .replace('image/png', 'image/octet-stream') + const link = document.createElement('a') + link.setAttribute('download', `Bluesky 10M Users.png`) + link.setAttribute('href', imgHref) + link.click() + logEvent('tmd:download', {}) + } + }, [uri]) + + /* + * Canvas stuff + */ + const imageRef = React.useRef(null) + const captureInProgress = React.useRef(false) + const onCanvasReady = React.useCallback(async () => { + if ( + imageRef.current && + imageRef.current.capture && + !captureInProgress.current + ) { + captureInProgress.current = true + const uri = await imageRef.current.capture() + setUri(uri) + } + }, [setUri]) + const canvas = isLoadingData ? null : ( + + + + + + + + + + + + + + {/* Centered content */} + + + + Celebrating {formatCount(i18n, 10000000)} users + {' '} + 🎉 + + + + # + + + {i18n.number(userNumber)} + + + + {Badge && ( + + + + )} + + {/* End centered content */} + + + + {/* + + */} + + + {sanitizeDisplayName( + profile.displayName || + sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + + + + {sanitizeHandle(profile.handle, '@')} + + + {profile.createdAt && ( + + + Joined{' '} + {i18n.date(profile.createdAt, { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + + )} + + + + + + + + + + + + ) + + return ( + + + + + + + {error ? ( + + + (╯°□°)â•Żïž” ┻━┻ + + + {error} + + + ) : isLoadingData || isLoadingImage ? ( + + ) : ( + + + + )} + + + + {canvas} + + + + Thanks for being one of our first 10 million users. + + + + + Together, we're rebuilding the social internet. We're glad + you're here. + + + + + + + + Brag a little! + + + + + + + + + + + + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx new file mode 100644 index 0000000000..7816960707 --- /dev/null +++ b/src/components/dialogs/nuxs/index.tsx @@ -0,0 +1,151 @@ +import React from 'react' + +import {useGate} from '#/lib/statsig/statsig' +import {logger} from '#/logger' +import { + Nux, + useNuxs, + useRemoveNuxsMutation, + useUpsertNuxMutation, +} from '#/state/queries/nuxs' +import {useSession} from '#/state/session' +import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' +import {TenMillion} from '#/components/dialogs/nuxs/TenMillion' +import {IS_DEV} from '#/env' + +type Context = { + activeNux: Nux | undefined + dismissActiveNux: () => void +} + +/** + * If we fail to complete a NUX here, it may show again on next reload, + * or if prefs state updates. If `true`, this fallback ensures that the last + * shown NUX won't show again, at least for this session. + * + * This is temporary, and only needed for the 10Milly dialog rn, since we + * aren't snoozing that one in device storage. + */ +let __isSnoozedFallback = false + +const queuedNuxs: { + id: Nux + enabled(props: {gate: ReturnType}): boolean + /** + * TEMP only intended for use with the 10Milly dialog rn, since there are no + * other NUX dialogs configured + */ + unsafe_disableSnooze: boolean +}[] = [ + { + id: Nux.TenMillionDialog, + enabled({gate}) { + return gate('ten_million_dialog') + }, + unsafe_disableSnooze: true, + }, +] + +const Context = React.createContext({ + activeNux: undefined, + dismissActiveNux: () => {}, +}) + +export function useNuxDialogContext() { + return React.useContext(Context) +} + +export function NuxDialogs() { + const {hasSession} = useSession() + return hasSession ? : null +} + +function Inner() { + const gate = useGate() + const {nuxs} = useNuxs() + const [snoozed, setSnoozed] = React.useState(() => { + return isSnoozed() + }) + const [activeNux, setActiveNux] = React.useState() + const {mutateAsync: upsertNux} = useUpsertNuxMutation() + const {mutate: removeNuxs} = useRemoveNuxsMutation() + + const snoozeNuxDialog = React.useCallback(() => { + snooze() + setSnoozed(true) + }, [setSnoozed]) + + const dismissActiveNux = React.useCallback(() => { + if (!activeNux) return + setActiveNux(undefined) + }, [activeNux, setActiveNux]) + + if (IS_DEV && typeof window !== 'undefined') { + // @ts-ignore + window.clearNuxDialog = (id: Nux) => { + if (!IS_DEV || !id) return + removeNuxs([id]) + unsnooze() + } + } + + React.useEffect(() => { + if (__isSnoozedFallback) return + if (snoozed) return + if (!nuxs) return + + for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) { + const nux = nuxs.find(nux => nux.id === id) + + // check if completed first + if (nux && nux.completed) continue + + // then check gate (track exposure) + if (!enabled({gate})) continue + + // we have a winner + setActiveNux(id) + + /** + * TEMP only intended for use with the 10Milly dialog rn, since there are no + * other NUX dialogs configured + */ + if (!unsafe_disableSnooze) { + // immediately snooze for a day + snoozeNuxDialog() + } + + // immediately update remote data (affects next reload) + upsertNux({ + id, + completed: true, + data: undefined, + }).catch(e => { + logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, { + safeMessage: e.message, + }) + /* + * TEMP only intended for use with the 10Milly dialog rn + */ + if (unsafe_disableSnooze) { + __isSnoozedFallback = true + } + }) + + break + } + }, [nuxs, snoozed, snoozeNuxDialog, upsertNux, gate]) + + const ctx = React.useMemo(() => { + return { + activeNux, + dismissActiveNux, + } + }, [activeNux, dismissActiveNux]) + + return ( + + {activeNux === Nux.TenMillionDialog && } + + ) +} diff --git a/src/components/dialogs/nuxs/snoozing.ts b/src/components/dialogs/nuxs/snoozing.ts new file mode 100644 index 0000000000..91effd0501 --- /dev/null +++ b/src/components/dialogs/nuxs/snoozing.ts @@ -0,0 +1,22 @@ +import {simpleAreDatesEqual} from '#/lib/strings/time' +import {device} from '#/storage' + +export function snooze() { + device.set(['lastNuxDialog'], new Date().toISOString()) +} + +export function unsnooze() { + device.set(['lastNuxDialog'], undefined) +} + +export function isSnoozed() { + const lastNuxDialog = device.get(['lastNuxDialog']) + if (!lastNuxDialog) return false + const last = new Date(lastNuxDialog) + const now = new Date() + // already snoozed today + if (simpleAreDatesEqual(last, now)) { + return true + } + return false +} diff --git a/src/components/icons/Download.tsx b/src/components/icons/Download.tsx new file mode 100644 index 0000000000..86b4942864 --- /dev/null +++ b/src/components/icons/Download.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Download_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/video/PlayButtonIcon.tsx b/src/components/video/PlayButtonIcon.tsx index 90e93f744b..8e0a6bb7a0 100644 --- a/src/components/video/PlayButtonIcon.tsx +++ b/src/components/video/PlayButtonIcon.tsx @@ -4,7 +4,7 @@ import {View} from 'react-native' import {atoms as a, useTheme} from '#/alf' import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' -export function PlayButtonIcon({size = 36}: {size?: number}) { +export function PlayButtonIcon({size = 32}: {size?: number}) { const t = useTheme() const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975 const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25 diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 6db96a8d63..f3ac45b6e3 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -2,6 +2,7 @@ import { AppBskyFeedDefs, AppBskyFeedGetFeed as GetCustomFeed, BskyAgent, + jsonStringToLex, } from '@atproto/api' import {getContentLanguages} from '#/state/preferences/languages' @@ -111,7 +112,7 @@ async function loggedOutFetch({ }&limit=${limit}&lang=${contentLangs}`, {method: 'GET', headers: {'Accept-Language': contentLangs}}, ) - let data = res.ok ? await res.json() : null + let data = res.ok ? jsonStringToLex(await res.text()) : null if (data?.feed?.length) { return { success: true, @@ -126,7 +127,7 @@ async function loggedOutFetch({ }&limit=${limit}`, {method: 'GET', headers: {'Accept-Language': ''}}, ) - data = res.ok ? await res.json() : null + data = res.ok ? jsonStringToLex(await res.text()) : null if (data?.feed?.length) { return { success: true, diff --git a/src/lib/canvas.ts b/src/lib/canvas.ts new file mode 100644 index 0000000000..760c0e67f3 --- /dev/null +++ b/src/lib/canvas.ts @@ -0,0 +1,15 @@ +export const getCanvas = (base64: string): Promise => { + return new Promise(resolve => { + const image = new Image() + image.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + + const ctx = canvas.getContext('2d') + ctx?.drawImage(image, 0, 0) + resolve(canvas) + } + image.src = base64 + }) +} diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 8cccda48fb..67f1c2c386 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -71,7 +71,7 @@ export function useIntentHandler() { }, [incomingUrl, composeIntent, verifyEmailIntent]) } -function useComposeIntent() { +export function useComposeIntent() { const closeAllActiveElements = useCloseAllActiveElements() const {openComposer} = useComposerControls() const {hasSession} = useSession() @@ -97,6 +97,10 @@ function useComposeIntent() { if (part.includes('https://') || part.includes('http://')) { return false } + console.log({ + part, + text: VALID_IMAGE_REGEX.test(part), + }) // We also should just filter out cases that don't have all the info we need return VALID_IMAGE_REGEX.test(part) }) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 1871894902..c9bc8fefb2 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -225,4 +225,8 @@ export type LogEvents = { 'test:gate1:sometimes': {} 'test:gate2:always': {} 'test:gate2:sometimes': {} + + 'tmd:share': {} + 'tmd:download': {} + 'tmd:post': {} } diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 7966767d1b..909b93e6be 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,3 +1,5 @@ export type Gate = // Keep this alphabetic please. - 'debug_show_feedcontext' | 'suggested_feeds_interstitial' + | 'debug_show_feedcontext' + | 'suggested_feeds_interstitial' + | 'ten_million_dialog' diff --git a/src/locale/locales/ja/messages.po b/src/locale/locales/ja/messages.po index 61dc1394dd..b1c03b59e3 100644 --- a/src/locale/locales/ja/messages.po +++ b/src/locale/locales/ja/messages.po @@ -115,7 +115,7 @@ msgstr "<0><1>テキă‚čトべタグ侭ぼ{0}" msgid "{0} joined this week" msgstr "今週、{0}äșșăŒć‚ćŠ ă—ăŸă—ăŸ" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:593 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:637 msgid "{0} of {1}" msgstr "{0} / {1}" @@ -123,7 +123,7 @@ msgstr "{0} / {1}" msgid "{0} people have used this starter pack!" msgstr "{0}äșșがこぼă‚čă‚żăƒŒă‚żăƒŒăƒ‘ăƒƒă‚Żă‚’äœżç”šă—ăŸă—ăŸïŒ" -#: src/view/com/util/UserAvatar.tsx:419 +#: src/view/com/util/UserAvatar.tsx:423 msgid "{0}'s avatar" msgstr "{0}ăźă‚ąăƒă‚żăƒŒ" @@ -372,7 +372,7 @@ msgstr "ă‚ąă‚«ă‚Šăƒłăƒˆă‚’èżœćŠ " msgid "Add alt text" msgstr "ALTテキă‚čăƒˆă‚’èżœćŠ " -#: src/view/com/composer/videos/SubtitleDialog.tsx:107 +#: src/view/com/composer/videos/SubtitleDialog.tsx:109 msgid "Add alt text (optional)" msgstr "ALTテキă‚čăƒˆă‚’èżœćŠ ïŒˆă‚Șăƒ—ă‚·ăƒ§ăƒłïŒ‰" @@ -493,9 +493,9 @@ msgid "ALT" msgstr "ALT" #: src/view/com/composer/GifAltText.tsx:144 -#: src/view/com/composer/videos/SubtitleDialog.tsx:54 -#: src/view/com/composer/videos/SubtitleDialog.tsx:102 -#: src/view/com/composer/videos/SubtitleDialog.tsx:106 +#: src/view/com/composer/videos/SubtitleDialog.tsx:56 +#: src/view/com/composer/videos/SubtitleDialog.tsx:104 +#: src/view/com/composer/videos/SubtitleDialog.tsx:108 #: src/view/com/modals/EditImage.tsx:316 #: src/view/screens/AccessibilitySettings.tsx:87 msgid "Alt text" @@ -522,11 +522,11 @@ msgstr "ä»„ć‰ăźăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚č{0}ă«ăƒĄăƒŒăƒ«ăŒé€äżĄă•ă‚ŒăŸă—ăŸă€‚ msgid "An error has occurred" msgstr "ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸ" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:369 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:413 msgid "An error occurred" msgstr "ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸ" -#: src/state/queries/video/video.ts:227 +#: src/state/queries/video/video.ts:232 msgid "An error occurred while compressing the video." msgstr "ビデă‚Șăźćœ§çžźäž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚" @@ -534,11 +534,11 @@ msgstr "ビデă‚Șăźćœ§çžźäž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚" msgid "An error occurred while generating your starter pack. Want to try again?" msgstr "ă‚čă‚żăƒŒă‚żăƒŒăƒ‘ăƒƒă‚Żăźç”Ÿæˆäž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚ć†ćșŠè©Šă—ăŸă™ă‹ïŒŸ" -#: src/view/com/util/post-embeds/VideoEmbed.tsx:213 +#: src/view/com/util/post-embeds/VideoEmbed.tsx:215 msgid "An error occurred while loading the video. Please try again later." msgstr "ビデă‚ȘたèȘ­ăżèŸŒăżæ™‚ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚æ™‚é–“ă‚’ăŠă„ăŠă‚‚ă†äž€ćșŠăŠè©Šă—ăă ă•ă„ă€‚" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:170 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:174 msgid "An error occurred while loading the video. Please try again." msgstr "ビデă‚ȘたèȘ­ăżèŸŒăżæ™‚ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚ă‚‚ă†äž€ćșŠăŠè©Šă—ăă ă•ă„ă€‚" @@ -547,7 +547,7 @@ msgstr "ビデă‚ȘたèȘ­ăżèŸŒăżæ™‚ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚ă‚‚ă†äž€ msgid "An error occurred while saving the QR code!" msgstr "QRă‚łăƒŒăƒ‰ăźäżć­˜äž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸïŒ" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:61 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:68 msgid "An error occurred while selecting the video" msgstr "ビデă‚ȘăźéžæŠžäž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸ" @@ -556,7 +556,7 @@ msgstr "ビデă‚ȘăźéžæŠžäž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸ" msgid "An error occurred while trying to follow all" msgstr "すăčăŠăƒ•ă‚©ăƒ­ăƒŒă—ă‚ˆă†ăšă—ăŸă‚‰ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸ" -#: src/state/queries/video/video.ts:194 +#: src/state/queries/video/video.ts:199 msgid "An error occurred while uploading the video." msgstr "ビデă‚Șăźă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰äž­ă«ă‚šăƒ©ăƒŒăŒç™șç”Ÿă—ăŸă—ăŸă€‚" @@ -702,7 +702,7 @@ msgstr "あăȘăŸăźăƒ•ă‚ŁăƒŒăƒ‰ă‹ă‚‰{0}ă‚’ć‰Šé™€ă—ăŠă‚‚ă‚ˆă‚ă—ă„ă§ă™ă‹ msgid "Are you sure you want to remove this from your feeds?" msgstr "æœŹćœ“ă«ă“ăźăƒ•ă‚ŁăƒŒăƒ‰ă‚’ă‚ăȘăŸăźăƒ•ă‚ŁăƒŒăƒ‰ă‹ă‚‰ć‰Šé™€ă—ăŸă„ă§ă™ă‹ïŒŸ" -#: src/view/com/composer/Composer.tsx:837 +#: src/view/com/composer/Composer.tsx:841 msgid "Are you sure you'd like to discard this draft?" msgstr "æœŹćœ“ă«ă“ăźäž‹æ›žăă‚’ç ŽæŁ„ă—ăŸă™ă‹ïŒŸ" @@ -844,6 +844,10 @@ msgstr "Bluesky は、ホă‚čティング ăƒ—ăƒ­ăƒă‚€ăƒ€ăƒŒă‚’éžæŠžă§ăă‚‹ă‚Ș msgid "Bluesky is better with friends!" msgstr "BlueskyăŻć‹é”ăšäž€ç·’ăźă»ă†ăŒæ„œă—ă„ïŒ" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:201 +msgid "Bluesky now has over 10 million users, and I was #{0}!" +msgstr "Bluesky ăźăƒŠăƒŒă‚¶ăƒŒæ•°ăŻçŸćœš 1,000 侇äșșă‚’è¶…ăˆăŠăŠă‚Šă€ç§ăŻ #{0} でした。" + #: src/components/StarterPack/ProfileStarterPacks.tsx:282 msgid "Bluesky will choose a set of recommended accounts from people in your network." msgstr "BlueskyはあăȘăŸăźă€ăȘăŒăŁăŠă„ă‚‹ăƒŠăƒŒă‚¶ăƒŒă‹ă‚‰ăŠă™ă™ă‚ăźă‚ąă‚«ă‚Šăƒłăƒˆă‚’éžăłăŸă™ă€‚" @@ -865,6 +869,10 @@ msgstr "ç”»ćƒăźăŒă‹ă—ăšăƒ•ă‚ŁăƒŒăƒ‰ă‹ă‚‰ăźăƒ•ă‚Łăƒ«ă‚żăƒȘング" msgid "Books" msgstr "曞籍" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:583 +msgid "Brag a little!" +msgstr "ちょっべè‡Șæ…ąă—ăŠăżă‚ˆă†ïŒ" + #: src/components/FeedInterstitials.tsx:346 msgid "Browse more accounts on the Explore page" msgstr "æ€œçŽąăƒšăƒŒă‚žă§ă•ă‚‰ă«ă‚ąă‚«ă‚Šăƒłăƒˆă‚’èŠ‹ă‚‹" @@ -927,8 +935,8 @@ msgstr "è‹±æ•°ć­—ă€ă‚čăƒšăƒŒă‚čă€ăƒă‚€ăƒ•ăƒłă€ă‚ąăƒłăƒ€ăƒŒă‚čコケぼみが #: src/components/Prompt.tsx:124 #: src/components/TagMenu/index.tsx:282 #: src/screens/Deactivated.tsx:161 -#: src/view/com/composer/Composer.tsx:590 -#: src/view/com/composer/Composer.tsx:605 +#: src/view/com/composer/Composer.tsx:595 +#: src/view/com/composer/Composer.tsx:610 #: src/view/com/modals/ChangeEmail.tsx:213 #: src/view/com/modals/ChangeEmail.tsx:215 #: src/view/com/modals/ChangeHandle.tsx:148 @@ -996,14 +1004,18 @@ msgstr "ăƒȘăƒłă‚Żć…ˆăźă‚Šă‚§ăƒ–ă‚”ă‚€ăƒˆă‚’é–‹ăă“ăšă‚’ă‚­ăƒŁăƒłă‚»ăƒ«" msgid "Cannot interact with a blocked user" msgstr "ăƒ–ăƒ­ăƒƒă‚Żă—ăŸăƒŠăƒŒă‚¶ăƒŒăšăŻă‚„ă‚Šăšă‚Šă§ăăŸă›ă‚“" -#: src/view/com/composer/videos/SubtitleDialog.tsx:133 +#: src/view/com/composer/videos/SubtitleDialog.tsx:135 msgid "Captions (.vtt)" msgstr "ă‚­ăƒŁăƒ—ă‚·ăƒ§ăƒłïŒˆ.vtt" -#: src/view/com/composer/videos/SubtitleDialog.tsx:54 +#: src/view/com/composer/videos/SubtitleDialog.tsx:56 msgid "Captions & alt text" msgstr "ă‚­ăƒŁăƒ—ă‚·ăƒ§ăƒłïŒ†ALTテキă‚čト" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:336 +msgid "Celebrating {0} users" +msgstr "{0} äșșăźăƒŠăƒŒă‚¶ăƒŒă‚’ç„çŠ" + #: src/view/com/modals/VerifyEmail.tsx:160 msgid "Change" msgstr "ć€‰æ›Ž" @@ -1238,7 +1250,7 @@ msgstr "例郹ぼナビă‚ČăƒŒă‚·ăƒ§ăƒłăƒăƒŒă‚’é–‰ă˜ă‚‹" msgid "Closes password update alert" msgstr "パă‚čăƒŻăƒŒăƒ‰æ›Žæ–°ă‚ąăƒ©ăƒŒăƒˆă‚’é–‰ă˜ă‚‹" -#: src/view/com/composer/Composer.tsx:602 +#: src/view/com/composer/Composer.tsx:607 msgid "Closes post composer and discards post draft" msgstr "æŠ•çšżăźç·šé›†ç”»éąă‚’é–‰ă˜ăŠäž‹æ›žăă‚’ć‰Šé™€ă™ă‚‹" @@ -1277,7 +1289,7 @@ msgstr "ćˆæœŸèš­ćźšă‚’ćźŒäș†ă—ăŠă‚ąă‚«ă‚Šăƒłăƒˆă‚’äœżă„ć§‹ă‚ă‚‹" msgid "Complete the challenge" msgstr "テă‚čトをクăƒȘケしどください" -#: src/view/com/composer/Composer.tsx:710 +#: src/view/com/composer/Composer.tsx:715 msgid "Compose posts up to {MAX_GRAPHEME_LENGTH} characters in length" msgstr "{MAX_GRAPHEME_LENGTH}æ–‡ć­—ăŸă§ăźæŠ•çšżă‚’äœœæˆ" @@ -1495,7 +1507,7 @@ msgstr "ăƒȘă‚čトたèȘ­ăżèŸŒăżă«ć€±æ•—ă—ăŸă—ăŸ" msgid "Could not mute chat" msgstr "ăƒăƒŁăƒƒăƒˆăźăƒŸăƒ„ăƒŒăƒˆă«ć€±æ•—ă—ăŸă—ăŸ" -#: src/view/com/composer/videos/VideoPreview.web.tsx:45 +#: src/view/com/composer/videos/VideoPreview.web.tsx:56 msgid "Could not process your video" msgstr "ビデă‚Șă‚’ć‡Šç†ă§ăăŸă›ă‚“ă§ă—ăŸ" @@ -1604,7 +1616,7 @@ msgstr "ăƒ€ăƒŒă‚ŻăƒąăƒŒăƒ‰" msgid "Dark theme" msgstr "ăƒ€ăƒŒă‚Żăƒ†ăƒŒăƒž" -#: src/screens/Signup/StepInfo/index.tsx:191 +#: src/screens/Signup/StepInfo/index.tsx:192 msgid "Date of birth" msgstr "生ćčŽæœˆæ—„" @@ -1738,7 +1750,7 @@ msgstr "ćŒ•ç”šæŠ•çšżă‚’ćˆ‡ă‚Šé›ąă—ăŸă™ă‹ïŒŸ" msgid "Dialog: adjust who can interact with this post" msgstr "ăƒ€ă‚€ă‚ąăƒ­ă‚°ïŒšă“ăźæŠ•çšżă«èȘ°ăŒććżœă§ăă‚‹ă‹èȘżæ•Ž" -#: src/view/com/composer/Composer.tsx:351 +#: src/view/com/composer/Composer.tsx:356 msgid "Did you want to say anything?" msgstr "ăȘă«ă‹èš€ă„ăŸă„ă“ăšăŻă‚ăŁăŸïŒŸ" @@ -1763,7 +1775,7 @@ msgstr "ăƒĄăƒŒăƒ«ă§ăźïŒ’èŠçŽ èȘèšŒă‚’焥ćŠč挖" msgid "Disable haptic feedback" msgstr "è§ŠèŠšăƒ•ă‚ŁăƒŒăƒ‰ăƒăƒƒă‚Żă‚’ç„ĄćŠč挖" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:335 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:379 msgid "Disable subtitles" msgstr "ă‚”ăƒ–ă‚żă‚€ăƒˆăƒ«ïŒˆć­—ćč•ïŒ‰ă‚’ç„ĄćŠčにする" @@ -1776,11 +1788,11 @@ msgstr "ă‚”ăƒ–ă‚żă‚€ăƒˆăƒ«ïŒˆć­—ćč•ïŒ‰ă‚’ç„ĄćŠčにする" msgid "Disabled" msgstr "無ćŠč" -#: src/view/com/composer/Composer.tsx:839 +#: src/view/com/composer/Composer.tsx:843 msgid "Discard" msgstr "ç ŽæŁ„" -#: src/view/com/composer/Composer.tsx:836 +#: src/view/com/composer/Composer.tsx:840 msgid "Discard draft?" msgstr "äž‹æ›žăă‚’ć‰Šé™€ă—ăŸă™ă‹ïŒŸ" @@ -1806,7 +1818,7 @@ msgstr "æ–°ă—ă„ăƒ•ă‚ŁăƒŒăƒ‰ă‚’æŽąă™" #~ msgid "Dismiss" #~ msgstr "æ¶ˆă™" -#: src/view/com/composer/Composer.tsx:1106 +#: src/view/com/composer/Composer.tsx:1110 msgid "Dismiss error" msgstr "ă‚šăƒ©ăƒŒă‚’æ¶ˆă™" @@ -1858,8 +1870,8 @@ msgstr "ăƒ‰ăƒĄă‚€ăƒłă‚’çąșèȘă—ăŸă—ăŸïŒ" #: src/screens/Onboarding/StepProfile/index.tsx:325 #: src/view/com/auth/server-input/index.tsx:169 #: src/view/com/auth/server-input/index.tsx:170 -#: src/view/com/composer/videos/SubtitleDialog.tsx:167 -#: src/view/com/composer/videos/SubtitleDialog.tsx:177 +#: src/view/com/composer/videos/SubtitleDialog.tsx:171 +#: src/view/com/composer/videos/SubtitleDialog.tsx:181 #: src/view/com/modals/AddAppPasswords.tsx:243 #: src/view/com/modals/AltImage.tsx:141 #: src/view/com/modals/crop-image/CropImage.web.tsx:177 @@ -1891,6 +1903,10 @@ msgstr "Blueskyă‚’ăƒ€ă‚Šăƒłăƒ­ăƒŒăƒ‰" msgid "Download CAR file" msgstr "CARăƒ•ă‚Ąă‚€ăƒ«ă‚’ăƒ€ă‚Šăƒłăƒ­ăƒŒăƒ‰" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:591 +msgid "Download image" +msgstr "ç”»ćƒă‚’ăƒ€ă‚Šăƒłăƒ­ăƒŒăƒ‰" + #: src/view/com/composer/text-input/TextInput.web.tsx:269 msgid "Drop to add images" msgstr "ăƒ‰ăƒ­ăƒƒăƒ—ă—ăŠç”»ćƒă‚’èżœćŠ ă™ă‚‹" @@ -1952,7 +1968,7 @@ msgctxt "action" msgid "Edit" msgstr "線集" -#: src/view/com/util/UserAvatar.tsx:328 +#: src/view/com/util/UserAvatar.tsx:332 #: src/view/com/util/UserBanner.tsx:92 msgid "Edit avatar" msgstr "ă‚ąăƒă‚żăƒŒă‚’ç·šé›†" @@ -2112,7 +2128,7 @@ msgstr "有ćŠčă«ă™ă‚‹ăƒĄăƒ‡ă‚Łă‚ąăƒ—ăƒŹă‚€ăƒ€ăƒŒ" msgid "Enable priority notifications" msgstr "ć„Șć…ˆé€šçŸ„ă‚’æœ‰ćŠčにする" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:336 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:380 msgid "Enable subtitles" msgstr "ă‚”ăƒ–ă‚żă‚€ăƒˆăƒ«ïŒˆć­—ćč•ïŒ‰ă‚’æœ‰ćŠčにする" @@ -2130,7 +2146,7 @@ msgstr "有ćŠč" msgid "End of feed" msgstr "ăƒ•ă‚ŁăƒŒăƒ‰ăźç”‚ă‚ă‚Š" -#: src/view/com/composer/videos/SubtitleDialog.tsx:157 +#: src/view/com/composer/videos/SubtitleDialog.tsx:161 msgid "Ensure you have selected a language for each subtitle file." msgstr "搄歗ćč•ăƒ•ă‚Ąă‚€ăƒ«ă«èš€èȘžăŒéžæŠžă•ă‚ŒăŠă‚‹ă“ăšă‚’çąșèȘă—ăŠăă ă•ă„ă€‚" @@ -2232,7 +2248,7 @@ msgstr "ăƒ•ă‚©ăƒ­ăƒŒă—ăŠă„ă‚‹ăƒŠăƒŒă‚¶ăƒŒăŻé™€ć€–" msgid "Excludes users you follow" msgstr "ăƒ•ă‚©ăƒ­ăƒŒă—ăŠă„ă‚‹ăƒŠăƒŒă‚¶ăƒŒăŻé™€ć€–" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:353 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:397 msgid "Exit fullscreen" msgstr "ć…šç”»éąèĄšç€șを甂äș†" @@ -2667,7 +2683,7 @@ msgctxt "from-feed" msgid "From <0/>" msgstr "<0/>から" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:354 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:398 msgid "Fullscreen" msgstr "ć…šç”»éąèĄšç€ș" @@ -2696,7 +2712,7 @@ msgstr "開構" msgid "Getting started" msgstr "ć…„é–€" -#: src/components/MediaPreview.tsx:119 +#: src/components/MediaPreview.tsx:120 msgid "GIF" msgstr "GIF" @@ -3098,7 +3114,7 @@ msgstr "æ‹›ćŸ…ă€ăŸă ă—ć€‹äșș的ăȘもぼ" msgid "It's just you right now! Add more people to your starter pack by searching above." msgstr "今はあăȘăŸă ă‘ïŒäžŠă§æ€œçŽąă—ăŠă‚čă‚żăƒŒă‚żăƒŒăƒ‘ăƒƒă‚Żă«ă‚ˆă‚Šć€šăăźăƒŠăƒŒă‚¶ăƒŒă‚’èżœćŠ ă—ăŠăă ă•ă„ă€‚" -#: src/view/com/composer/Composer.tsx:1125 +#: src/view/com/composer/Composer.tsx:1129 msgid "Job ID: {0}" msgstr "ゾョブID{0}" @@ -3117,6 +3133,10 @@ msgstr "Blueskyă«ć‚ćŠ " msgid "Join the conversation" msgstr "äŒšè©±ă«ć‚ćŠ " +#: src/components/dialogs/nuxs/TenMillion/index.tsx:461 +msgid "Joined {0}" +msgstr "{0} 揂抠" + #: src/screens/Onboarding/index.tsx:21 #: src/screens/Onboarding/state.ts:89 msgid "Journalism" @@ -3580,8 +3600,12 @@ msgstr "映画" msgid "Music" msgstr "éŸłæ„œ" +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389 +msgctxt "video" +msgid "Mute" +msgstr "" + #: src/components/TagMenu/index.tsx:263 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:345 msgid "Mute" msgstr "ăƒŸăƒ„ăƒŒăƒˆ" @@ -4042,6 +4066,10 @@ msgstr "ăĄă‚‡ăŁăšïŒ" msgid "Oh no! Something went wrong." msgstr "ăĄă‚‡ăŁăšïŒäœ•ă‚‰ă‹ăźć•éĄŒăŒç™ș生したようです。" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:171 +msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🩋" +msgstr "ă‚ă‚ă€æź‹ćż”ïŒć…±æœ‰ă™ă‚‹ăŸă‚ăźç”»ćƒă‚’ç”Ÿæˆă§ăăŸă›ă‚“ă§ă—ăŸă€‚ă”ćź‰ćżƒăă ă•ă„ă€ă“ă“ă«æ„ăŠăă‚ŒăŠćŹ‰ă—ă„ă§ă™đŸŠ‹" + #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339 msgid "OK" msgstr "OK" @@ -4062,7 +4090,7 @@ msgstr "<0><1/><2><3/>" msgid "Onboarding reset" msgstr "ă‚ȘăƒłăƒœăƒŒăƒ‡ă‚Łăƒłă‚°ăźăƒȘă‚»ăƒƒăƒˆ" -#: src/view/com/composer/Composer.tsx:667 +#: src/view/com/composer/Composer.tsx:672 msgid "One or more images is missing alt text." msgstr "ïŒ‘ă€ă‚‚ă—ăăŻè€‡æ•°ăźç”»ćƒă«ALTテキă‚čăƒˆăŒă‚ă‚ŠăŸă›ă‚“ă€‚" @@ -4113,8 +4141,8 @@ msgid "Open conversation options" msgstr "äŒšè©±ăźă‚Șăƒ—ă‚·ăƒ§ăƒłă‚’é–‹ă" #: src/screens/Messages/Conversation/MessageInput.web.tsx:165 -#: src/view/com/composer/Composer.tsx:819 -#: src/view/com/composer/Composer.tsx:820 +#: src/view/com/composer/Composer.tsx:823 +#: src/view/com/composer/Composer.tsx:824 msgid "Open emoji picker" msgstr "ç””æ–‡ć­—ă‚’ć…„ćŠ›" @@ -4283,11 +4311,11 @@ msgid "Opens the threads preferences" msgstr "ă‚čăƒŹăƒƒăƒ‰ăźèš­ćźšă‚’é–‹ă" #: src/view/com/notifications/FeedItem.tsx:551 -#: src/view/com/util/UserAvatar.tsx:420 +#: src/view/com/util/UserAvatar.tsx:424 msgid "Opens this profile" msgstr "ăƒ—ăƒ­ăƒ•ă‚ŁăƒŒăƒ«ă‚’é–‹ă" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:81 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:88 msgid "Opens video picker" msgstr "ビデă‚ȘăźéžæŠžç”»éąă‚’é–‹ă" @@ -4365,11 +4393,11 @@ msgid "Password updated!" msgstr "パă‚čăƒŻăƒŒăƒ‰ăŒæ›Žæ–°ă•ă‚ŒăŸă—ăŸïŒ" #: src/view/com/util/post-embeds/GifEmbed.tsx:44 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:322 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:366 msgid "Pause" msgstr "äž€æ™‚ćœæ­ą" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:275 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:319 msgid "Pause video" msgstr "ビデă‚Șă‚’äž€æ™‚ćœæ­ą" @@ -4429,7 +4457,7 @@ msgid "Pinned to your feeds" msgstr "ăƒ•ă‚ŁăƒŒăƒ‰ă«ăƒ”ăƒłç•™ă‚ă—ăŸă—ăŸ" #: src/view/com/util/post-embeds/GifEmbed.tsx:44 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:323 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:367 msgid "Play" msgstr "憍生" @@ -4441,8 +4469,8 @@ msgstr "{0}を憍生" msgid "Play or pause the GIF" msgstr "GIFăźć†ç”Ÿă‚„äž€æ™‚ćœæ­ą" -#: src/view/com/util/post-embeds/VideoEmbed.tsx:187 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:276 +#: src/view/com/util/post-embeds/VideoEmbed.tsx:189 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:320 msgid "Play video" msgstr "ビデă‚Șを憍生" @@ -4514,7 +4542,7 @@ msgstr "@{0}ăšă—ăŠă‚”ă‚€ăƒłă‚€ăƒłă—ăŠăă ă•ă„" msgid "Please Verify Your Email" msgstr "ăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čをçąșèȘă—ăŠăă ă•ă„" -#: src/view/com/composer/Composer.tsx:355 +#: src/view/com/composer/Composer.tsx:360 msgid "Please wait for your link card to finish loading" msgstr "ăƒȘăƒłă‚Żă‚«ăƒŒăƒ‰ăŒèȘ­ăżèŸŒăŸă‚Œă‚‹ăŸă§ăŠćŸ…ăĄăă ă•ă„" @@ -4527,8 +4555,8 @@ msgstr "æ”żæČ»" msgid "Porn" msgstr "ăƒăƒ«ăƒŽ" -#: src/view/com/composer/Composer.tsx:642 -#: src/view/com/composer/Composer.tsx:649 +#: src/view/com/composer/Composer.tsx:647 +#: src/view/com/composer/Composer.tsx:654 msgctxt "action" msgid "Post" msgstr "æŠ•çšż" @@ -4698,11 +4726,11 @@ msgstr "ăƒŠăƒŒă‚¶ăƒŒă‚’äž€æ‹Źă§ăƒŸăƒ„ăƒŒăƒˆăŸăŸăŻăƒ–ăƒ­ăƒƒă‚Żă™ă‚‹ă€ć…Źé–‹ msgid "Public, shareable lists which can drive feeds." msgstr "ăƒ•ă‚ŁăƒŒăƒ‰ăšă—ăŠćˆ©ç”šă§ăă‚‹ă€ć…Źé–‹ă•ă‚ŒăŸć…±æœ‰ćŻèƒœăȘăƒȘă‚čト。" -#: src/view/com/composer/Composer.tsx:627 +#: src/view/com/composer/Composer.tsx:632 msgid "Publish post" msgstr "æŠ•çšżă‚’ć…Źé–‹" -#: src/view/com/composer/Composer.tsx:627 +#: src/view/com/composer/Composer.tsx:632 msgid "Publish reply" msgstr "èż”äżĄă‚’ć…Źé–‹" @@ -4832,7 +4860,7 @@ msgstr "ă‚ąă‚«ă‚Šăƒłăƒˆă‚’ć‰Šé™€" msgid "Remove attachment" msgstr "æ·»ä»˜ă‚’ć‰Šé™€" -#: src/view/com/util/UserAvatar.tsx:387 +#: src/view/com/util/UserAvatar.tsx:391 msgid "Remove Avatar" msgstr "ă‚ąăƒă‚żăƒŒă‚’ć‰Šé™€" @@ -4900,7 +4928,7 @@ msgstr "ćŒ•ç”šă‚’ć‰Šé™€" msgid "Remove repost" msgstr "ăƒȘポă‚čăƒˆă‚’ć‰Šé™€" -#: src/view/com/composer/videos/SubtitleDialog.tsx:260 +#: src/view/com/composer/videos/SubtitleDialog.tsx:264 msgid "Remove subtitle file" msgstr "歗ćč•ăƒ•ă‚Ąă‚€ăƒ«ă‚’ć‰Šé™€" @@ -4961,7 +4989,7 @@ msgstr "èż”äżĄă§ăăŸă›ă‚“" msgid "Replies to this post are disabled." msgstr "ă“ăźæŠ•çšżăžăźèż”äżĄăŻç„ĄćŠčćŒ–ă•ă‚ŒăŠă„ăŸă™ă€‚" -#: src/view/com/composer/Composer.tsx:640 +#: src/view/com/composer/Composer.tsx:645 msgctxt "action" msgid "Reply" msgstr "èż”äżĄ" @@ -5415,7 +5443,7 @@ msgstr "Blueskyăźæ±‚äșșă‚’èŠ‹ă‚‹" msgid "See this guide" msgstr "ă‚Źă‚€ăƒ‰ă‚’èŠ‹ă‚‹" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:587 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:631 msgid "Seek slider" msgstr "ă‚·ăƒŒă‚ŻăƒăƒŒ" @@ -5455,7 +5483,7 @@ msgstr "GIF「{0}ă€ă‚’éžă¶" msgid "Select how long to mute this word for." msgstr "ă“ăźăƒŻăƒŒăƒ‰ă‚’ă©ăźăă‚‰ă„ăźé–“ăƒŸăƒ„ăƒŒăƒˆă™ă‚‹ăźă‹ă‚’éžæŠžă€‚" -#: src/view/com/composer/videos/SubtitleDialog.tsx:245 +#: src/view/com/composer/videos/SubtitleDialog.tsx:249 msgid "Select language..." msgstr "蚀èȘžă‚’遞択 " @@ -5487,7 +5515,7 @@ msgstr "ć ±ć‘Šć…ˆăźăƒąăƒ‡ăƒŹăƒŒă‚·ăƒ§ăƒłă‚”ăƒŒăƒ“ă‚čを遾んでください" msgid "Select the service that hosts your data." msgstr "ăƒ‡ăƒŒă‚żă‚’ăƒ›ă‚čăƒˆă™ă‚‹ă‚”ăƒŒăƒ“ă‚čă‚’éžæŠžă—ăŸă™ă€‚" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:80 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:87 msgid "Select video" msgstr "ビデă‚Șă‚’éžæŠž" @@ -5503,7 +5531,7 @@ msgstr "登éŒČă•ă‚ŒăŸăƒ•ă‚ŁăƒŒăƒ‰ă«ć«ă‚ă‚‹èš€èȘžă‚’éžæŠžă—ăŸă™ă€‚éžæŠž msgid "Select your app language for the default text to display in the app." msgstr "ケプăƒȘă«èĄšç€șă•ă‚Œă‚‹ăƒ‡ăƒ•ă‚©ăƒ«ăƒˆăźăƒ†ă‚­ă‚čăƒˆăźèš€èȘžă‚’遞択" -#: src/screens/Signup/StepInfo/index.tsx:192 +#: src/screens/Signup/StepInfo/index.tsx:193 msgid "Select your date of birth" msgstr "生ćčŽæœˆæ—„ă‚’éžæŠž" @@ -5643,6 +5671,7 @@ msgstr "æ€§çš„èĄŒç‚șăŸăŸăŻæ€§çš„ăȘăƒŒăƒŒăƒ‰ă€‚" msgid "Sexually Suggestive" msgstr "æ€§çš„ă«ăă‚ă©ă„" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:607 #: src/components/StarterPack/QrCodeDialog.tsx:177 #: src/screens/StarterPack/StarterPackScreen.tsx:411 #: src/screens/StarterPack/StarterPackScreen.tsx:582 @@ -5679,6 +5708,14 @@ msgstr "ăšă«ă‹ăć…±æœ‰" msgid "Share feed" msgstr "ăƒ•ă‚ŁăƒŒăƒ‰ă‚’ć…±æœ‰" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:590 +msgid "Share image externally" +msgstr "ç”»ćƒă‚’ć€–éƒšă«ć…±æœ‰ă™ă‚‹" + +#: src/components/dialogs/nuxs/TenMillion/index.tsx:602 +msgid "Share image in post" +msgstr "æŠ•çšżă§ç”»ćƒă‚’ć…±æœ‰" + #: src/components/StarterPack/ShareDialog.tsx:124 #: src/components/StarterPack/ShareDialog.tsx:131 #: src/screens/StarterPack/StarterPackScreen.tsx:586 @@ -5950,8 +5987,8 @@ msgstr "äœ•ă‚‰ă‹ăźć•éĄŒăŒç™ș生したようăȘぼで、もう侀ćșŠăŠè©Šă— msgid "Something went wrong!" msgstr "äœ•ă‚‰ă‹ăźć•éĄŒăŒç™șç”Ÿă—ăŸă‚ˆă†ă§ă™ïŒ" -#: src/App.native.tsx:102 -#: src/App.web.tsx:83 +#: src/App.native.tsx:103 +#: src/App.web.tsx:84 msgid "Sorry! Your session expired. Please log in again." msgstr "ć€§ć€‰ç”łă—èšłă‚ă‚ŠăŸă›ă‚“ïŒă‚»ăƒƒă‚·ăƒ§ăƒłăźæœ‰ćŠčæœŸé™ăŒćˆ‡ă‚ŒăŸă—ăŸă€‚ă‚‚ă†äž€ćșŠăƒ­ă‚°ă‚€ăƒłă—ăŠăă ă•ă„ă€‚" @@ -6153,6 +6190,10 @@ msgstr "ă‚žăƒ§ăƒŒă‚Żă‚’èš€ăŁăŠïŒ" msgid "Tell us a little more" msgstr "ă‚‚ă†ć°‘ă—æ•™ăˆăŠ" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:487 +msgid "Ten Million" +msgstr "1000侇" + #: src/view/shell/desktop/RightNav.tsx:90 msgid "Terms" msgstr "æĄä»¶" @@ -6186,6 +6227,10 @@ msgstr "テキă‚čăƒˆăźć…„ćŠ›ăƒ•ă‚ŁăƒŒăƒ«ăƒ‰" msgid "Thank you. Your report has been sent." msgstr "ă‚ă‚ŠăŒăšă†ă”ă–ă„ăŸă™ă€‚ă‚ăȘăŸăźć ±ć‘ŠăŻé€äżĄă•ă‚ŒăŸă—ăŸă€‚" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:562 +msgid "Thanks for being one of our first 10 million users." +msgstr "æœ€ćˆăź 1,000 侇äșșăźăƒŠăƒŒă‚¶ăƒŒăź 1 äșșにăȘăŁăŠă„ăŸă ăă‚ă‚ŠăŒăšă†ă”ă–ă„ăŸă™ă€‚" + #: src/components/intents/VerifyEmailIntentDialog.tsx:74 msgid "Thanks, you have successfully verified your email address." msgstr "" @@ -6271,7 +6316,7 @@ msgstr "æŠ•çšżăŒć‰Šé™€ă•ă‚ŒăŸćŻèƒœæ€§ăŒă‚ă‚ŠăŸă™ă€‚" msgid "The Privacy Policy has been moved to <0/>" msgstr "ăƒ—ăƒ©ă‚€ăƒă‚·ăƒŒăƒăƒȘă‚·ăƒŒăŻ<0/>ă«ç§»ć‹•ă—ăŸă—ăŸ" -#: src/state/queries/video/video.ts:222 +#: src/state/queries/video/video.ts:227 msgid "The selected video is larger than 50MB." msgstr "éžæŠžă—ăŸăƒ“ăƒ‡ă‚Șたゔむă‚șが50MBă‚’è¶…ăˆăŠă„ăŸă™ă€‚" @@ -6608,7 +6653,7 @@ msgstr "ăƒĄăƒŒăƒ«ă§ăźïŒ’èŠçŽ èȘèšŒă‚’焥ćŠčă«ă™ă‚‹ă«ăŻă€ăƒĄăƒŒăƒ«ă‚ąăƒ‰ msgid "To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue." msgstr "äŒšè©±ă‚’ć ±ć‘Šă™ă‚‹ă«ăŻă€äŒšè©±ăźç”»éąă‹ă‚‰ăƒĄăƒƒă‚»ăƒŒă‚žăźă†ăĄăźäž€ă€ă‚’ć ±ć‘Šă—ăŠăă ă•ă„ă€‚ăă‚Œă«ă‚ˆăŁăŠć•éĄŒăźæ–‡è„ˆă‚’ăƒąăƒ‡ăƒŹăƒŒă‚żăƒŒăŒç†è§Łă§ăă‚‹ă‚ˆă†ă«ăȘă‚ŠăŸă™ă€‚" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:106 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:113 msgid "To upload videos to Bluesky, you must first verify your email." msgstr "Blueskyă«ăƒ“ăƒ‡ă‚Șă‚’ă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰ă™ă‚‹ă«ăŻă€ăŸăšăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čをçąșèȘă—ăȘくどはăȘă‚ŠăŸă›ă‚“ă€‚" @@ -6616,6 +6661,10 @@ msgstr "Blueskyă«ăƒ“ăƒ‡ă‚Șă‚’ă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰ă™ă‚‹ă«ăŻă€ăŸăšăƒĄăƒŒăƒ« msgid "To whom would you like to send this report?" msgstr "こた栱摊をèȘ°ă«é€ă‚ŠăŸă„ă§ă™ă‹ïŒŸ" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:566 +msgid "Together, we're rebuilding the social internet. We're glad you're here." +msgstr "ç§ăŸăĄăŻäž€ç·’ă«ă‚œăƒŒă‚·ăƒŁăƒ« ă‚€ăƒłă‚żăƒŒăƒăƒƒăƒˆă‚’ć†æ§‹çŻ‰ă—ăŠă„ăŸă™ă€‚ă”ć‚ćŠ ă„ăŸă ăă‚ă‚ŠăŒăšă†ă”ă–ă„ăŸă™ă€‚" + #: src/view/com/util/forms/DropdownButton.tsx:255 msgid "Toggle dropdown" msgstr "ăƒ‰ăƒ­ăƒƒăƒ—ăƒ€ă‚Šăƒłă‚’ćˆ‡ă‚Šæ›żăˆ" @@ -6739,8 +6788,12 @@ msgstr "ă‚ąă‚«ă‚Šăƒłăƒˆăźăƒ•ă‚©ăƒ­ăƒŒă‚’è§Łé™€" msgid "Unlike this feed" msgstr "ă“ăźăƒ•ă‚ŁăƒŒăƒ‰ă‹ă‚‰ă„ă„ă­ă‚’ć€–ă™" +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388 +msgctxt "video" +msgid "Unmute" +msgstr "" + #: src/components/TagMenu/index.tsx:263 -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:344 #: src/view/screens/ProfileList.tsx:689 msgid "Unmute" msgstr "ăƒŸăƒ„ăƒŒăƒˆă‚’è§Łé™€" @@ -6767,7 +6820,7 @@ msgstr "äŒšè©±ăźăƒŸăƒ„ăƒŒăƒˆă‚’è§Łé™€" msgid "Unmute thread" msgstr "ă‚čăƒŹăƒƒăƒ‰ăźăƒŸăƒ„ăƒŒăƒˆă‚’è§Łé™€" -#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:273 +#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:317 msgid "Unmute video" msgstr "ビデă‚ȘăźăƒŸăƒ„ăƒŒăƒˆă‚’è§Łé™€" @@ -6809,7 +6862,7 @@ msgstr "ă“ăźăƒ©ăƒ™ăƒ©ăƒŒăźç™»éŒČă‚’è§Łé™€" msgid "Unsubscribed from list" msgstr "ăƒȘă‚čăƒˆăźç™»éŒČă‚’è§Łé™€ă—ăŸă—ăŸ" -#: src/state/queries/video/video.ts:240 +#: src/state/queries/video/video.ts:245 msgid "Unsupported video type: {mimeType}" msgstr "ă‚”ăƒăƒŒăƒˆă—ăŠă„ăȘいビデă‚ȘćœąćŒïŒš{mimeType}" @@ -6846,20 +6899,20 @@ msgstr "ä»Łă‚ă‚Šă«ć†™çœŸă‚’ă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰" msgid "Upload a text file to:" msgstr "テキă‚čăƒˆăƒ•ă‚Ąă‚€ăƒ«ăźă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰ć…ˆïŒš" -#: src/view/com/util/UserAvatar.tsx:355 -#: src/view/com/util/UserAvatar.tsx:358 +#: src/view/com/util/UserAvatar.tsx:359 +#: src/view/com/util/UserAvatar.tsx:362 #: src/view/com/util/UserBanner.tsx:123 #: src/view/com/util/UserBanner.tsx:126 msgid "Upload from Camera" msgstr "ă‚«ăƒĄăƒ©ă‹ă‚‰ă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰" -#: src/view/com/util/UserAvatar.tsx:372 +#: src/view/com/util/UserAvatar.tsx:376 #: src/view/com/util/UserBanner.tsx:140 msgid "Upload from Files" msgstr "ăƒ•ă‚Ąă‚€ăƒ«ă‹ă‚‰ă‚ąăƒƒăƒ—ăƒ­ăƒŒăƒ‰" -#: src/view/com/util/UserAvatar.tsx:366 #: src/view/com/util/UserAvatar.tsx:370 +#: src/view/com/util/UserAvatar.tsx:374 #: src/view/com/util/UserBanner.tsx:134 #: src/view/com/util/UserBanner.tsx:138 msgid "Upload from Library" @@ -6988,7 +7041,7 @@ msgstr "ă“ăźă‚łăƒłăƒ†ăƒłăƒ„ă‚„ăƒ—ăƒ­ăƒ•ă‚ŁăƒŒăƒ«ă«ă„ă„ă­ă‚’ă—ăŠă„ă‚‹ăƒŠ msgid "Value:" msgstr "ć€€ïŒš" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:104 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:111 msgid "Verified email required" msgstr "ăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čたçąșèȘăŒćż…èЁ" @@ -7017,7 +7070,7 @@ msgstr "ăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čをçąșèȘ" msgid "Verify New Email" msgstr "æ–°ă—ă„ăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čをçąșèȘ" -#: src/view/com/composer/videos/SelectVideoBtn.tsx:108 +#: src/view/com/composer/videos/SelectVideoBtn.tsx:115 msgid "Verify now" msgstr "çąșèȘă™ă‚‹" @@ -7047,11 +7100,11 @@ msgstr "ビデă‚Șăźć‡Šç†ă«ć€±æ•—" msgid "Video Games" msgstr "ビデă‚Șă‚ČăƒŒăƒ " -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:163 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:167 msgid "Video not found." msgstr "ビデă‚ȘăŒèŠ‹ă€ă‹ă‚ŠăŸă›ă‚“ă€‚" -#: src/view/com/composer/videos/SubtitleDialog.tsx:99 +#: src/view/com/composer/videos/SubtitleDialog.tsx:101 msgid "Video settings" msgstr "ビデă‚Șăźèš­ćźš" @@ -7059,6 +7112,11 @@ msgstr "ビデă‚Șăźèš­ćźš" msgid "Video: {0}" msgstr "ビデă‚Ș{0}" +#: src/view/com/composer/videos/SelectVideoBtn.tsx:58 +#: src/view/com/composer/videos/VideoPreview.web.tsx:44 +msgid "Videos must be less than 60 seconds long" +msgstr "" + #: src/screens/Profile/Header/Shell.tsx:113 msgid "View {0}'s avatar" msgstr "{0}ăźă‚ąăƒă‚żăƒŒă‚’èĄšç€ș" @@ -7224,7 +7282,7 @@ msgstr "ć€§ć€‰ç”łă—èšłă‚ă‚ŠăŸă›ă‚“ăŒă€çŸćœšăƒŸăƒ„ăƒŒăƒˆă•ă‚ŒăŸăƒŻăƒŒăƒ‰ msgid "We're sorry, but your search could not be completed. Please try again in a few minutes." msgstr "ć€§ć€‰ç”łă—èšłă‚ă‚ŠăŸă›ă‚“ăŒă€æ€œçŽąă‚’ćźŒäș†ă§ăăŸă›ă‚“ă§ă—ăŸă€‚æ•°ćˆ†ćŸŒă«ć†è©ŠèĄŒă—ăŠăă ă•ă„ă€‚" -#: src/view/com/composer/Composer.tsx:417 +#: src/view/com/composer/Composer.tsx:422 msgid "We're sorry! The post you are replying to has been deleted." msgstr "ć€§ć€‰ç”łă—èšłă‚ă‚ŠăŸă›ă‚“ïŒèż”äżĄă—ă‚ˆă†ăšă—ăŠă„ă‚‹æŠ•çšżăŻć‰Šé™€ă•ă‚ŒăŸă—ăŸă€‚" @@ -7255,7 +7313,7 @@ msgstr "あăȘたぼă‚čă‚żăƒŒă‚żăƒŒăƒ‘ăƒƒă‚Żă‚’äœ•ăšć‘ŒăłăŸă„ă§ă™ă‹ïŒŸ" #: src/view/com/auth/SplashScreen.tsx:40 #: src/view/com/auth/SplashScreen.web.tsx:86 -#: src/view/com/composer/Composer.tsx:512 +#: src/view/com/composer/Composer.tsx:517 msgid "What's up?" msgstr "æœ€èż‘ă©ă†ïŒŸ" @@ -7322,11 +7380,11 @@ msgstr "ăƒŻă‚€ăƒ‰" msgid "Write a message" msgstr "ăƒĄăƒƒă‚»ăƒŒă‚žă‚’æ›žă" -#: src/view/com/composer/Composer.tsx:708 +#: src/view/com/composer/Composer.tsx:713 msgid "Write post" msgstr "æŠ•çšżă‚’æ›žă" -#: src/view/com/composer/Composer.tsx:511 +#: src/view/com/composer/Composer.tsx:516 #: src/view/com/post-thread/PostThreadComposePrompt.tsx:42 msgid "Write your reply" msgstr "èż”äżĄă‚’æ›žă" @@ -7646,15 +7704,19 @@ msgstr "あăȘăŸăźă‚ąă‚«ă‚Šăƒłăƒˆ" msgid "Your account has been deleted" msgstr "あăȘăŸăźă‚ąă‚«ă‚ŠăƒłăƒˆăŻć‰Šé™€ă•ă‚ŒăŸă—ăŸ" +#: src/state/queries/video/video.ts:185 +msgid "Your account is not yet old enough to upload videos. Please try again later." +msgstr "" + #: src/view/screens/Settings/ExportCarDialog.tsx:65 msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." msgstr "あăȘăŸăźă‚ąă‚«ă‚Šăƒłăƒˆăźć…Źé–‹ăƒ‡ăƒŒă‚żăźć…šèš˜éŒČを搫むăƒȘポゾトăƒȘは、「CARă€ăƒ•ă‚Ąă‚€ăƒ«ăšă—ăŠăƒ€ă‚Šăƒłăƒ­ăƒŒăƒ‰ă§ăăŸă™ă€‚ă“ăźăƒ•ă‚Ąă‚€ăƒ«ă«ăŻă€ç”»ćƒăȘă©ăźăƒĄăƒ‡ă‚Łă‚ąćŸ‹ă‚èŸŒăżă€ăŸăŸéžć…Źé–‹ăźăƒ‡ăƒŒă‚żăŻć«ăŸă‚ŒăŠă„ăȘă„ăŸă‚ă€ăă‚Œă‚‰ăŻć€‹ćˆ„ă«ć–ćŸ—ă™ă‚‹ćż…èŠăŒă‚ă‚ŠăŸă™ă€‚" -#: src/screens/Signup/StepInfo/index.tsx:180 +#: src/screens/Signup/StepInfo/index.tsx:181 msgid "Your birth date" msgstr "生ćčŽæœˆæ—„" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:167 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:171 msgid "Your browser does not support the video format. Please try a different browser." msgstr "ćˆ©ç”šäž­ăźăƒ–ăƒ©ă‚Šă‚¶ăŒă“ăźăƒ“ăƒ‡ă‚ȘćœąćŒă‚’ă‚”ăƒăƒŒăƒˆă—ăŠă„ăŸă›ă‚“ă€‚ä»–ăźăƒ–ăƒ©ă‚Šă‚¶ă‚’ăŠè©Šă—ăă ă•ă„ă€‚" @@ -7705,7 +7767,7 @@ msgstr "ăƒŸăƒ„ăƒŒăƒˆă—ăŸăƒŻăƒŒăƒ‰" msgid "Your password has been changed successfully!" msgstr "パă‚čăƒŻăƒŒăƒ‰ăźć€‰æ›ŽăŒćźŒäș†ă—ăŸă—ăŸïŒ" -#: src/view/com/composer/Composer.tsx:463 +#: src/view/com/composer/Composer.tsx:468 msgid "Your post has been published" msgstr "æŠ•çšżă‚’ć…Źé–‹ă—ăŸă—ăŸ" @@ -7721,7 +7783,7 @@ msgstr "あăȘăŸăźăƒ—ăƒ­ăƒ•ă‚ŁăƒŒăƒ«" msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in." msgstr "あăȘăŸăźăƒ—ăƒ­ăƒ•ă‚ŁăƒŒăƒ«ă€æŠ•çšżă€ăƒ•ă‚ŁăƒŒăƒ‰ă€ăă—ăŠăƒȘă‚čăƒˆăŻä»–ăźBlueskyăƒŠăƒŒă‚¶ăƒŒă«èŠ‹ăˆăȘくăȘă‚ŠăŸă™ă€‚ăƒ­ă‚°ă‚€ăƒłă™ă‚‹ă“ăšă§ă„ă€ă§ă‚‚ă‚ąă‚«ă‚Šăƒłăƒˆă‚’ć†æœ‰ćŠčćŒ–ă§ăăŸă™ă€‚" -#: src/view/com/composer/Composer.tsx:462 +#: src/view/com/composer/Composer.tsx:467 msgid "Your reply has been published" msgstr "èż”äżĄă‚’ć…Źé–‹ă—ăŸă—ăŸ" diff --git a/src/locale/locales/pt-BR/messages.po b/src/locale/locales/pt-BR/messages.po index c51e3e8266..d49528e9b5 100644 --- a/src/locale/locales/pt-BR/messages.po +++ b/src/locale/locales/pt-BR/messages.po @@ -139,7 +139,7 @@ msgstr "{0} pessoas jĂĄ usaram este pacote inicial!" #~ msgid "{0} your feeds" #~ msgstr "{0} seus feeds" -#: src/view/com/util/UserAvatar.tsx:419 +#: src/view/com/util/UserAvatar.tsx:423 msgid "{0}'s avatar" msgstr "Avatar de {0}" @@ -656,7 +656,7 @@ msgstr "Ocorreu um erro ao gerar seu pacote inicial. Quer tentar novamente?" msgid "An error occurred while loading the video. Please try again later." msgstr "Ocorreu um erro ao carregar o vĂ­deo. Tente novamente mais tarde." -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:171 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:174 msgid "An error occurred while loading the video. Please try again." msgstr "Ocorreu um erro ao carregar o vĂ­deo. Tente novamente." @@ -1005,6 +1005,10 @@ msgstr "Bluesky Ă© melhor com amigos!" #~ msgid "Bluesky is public." #~ msgstr "Bluesky Ă© pĂșblico." +#: src/components/dialogs/nuxs/TenMillion/index.tsx:201 +msgid "Bluesky now has over 10 million users, and I was #{0}!" +msgstr "O Bluesky agora tem mais de 10 milhĂ”es de usuĂĄrios, e eu tinha #{0}!" + #: src/components/StarterPack/ProfileStarterPacks.tsx:282 msgid "Bluesky will choose a set of recommended accounts from people in your network." msgstr "O Bluesky escolherĂĄ um conjunto de contas recomendadas dentre as pessoas em sua rede." @@ -1026,6 +1030,10 @@ msgstr "Desfocar imagens e filtrar dos feeds" msgid "Books" msgstr "Livros" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:583 +msgid "Brag a little!" +msgstr "Gabe-se um pouco!" + #: src/components/FeedInterstitials.tsx:346 msgid "Browse more accounts on the Explore page" msgstr "Navegue por mais contas na pĂĄgina Explorar" @@ -1173,6 +1181,10 @@ msgstr "Legendas (.vtt)" msgid "Captions & alt text" msgstr "Legendas e texto alt" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:336 +msgid "Celebrating {0} users" +msgstr "Comemorando {0} usuĂĄrios" + #: src/view/com/modals/VerifyEmail.tsx:160 msgid "Change" msgstr "Alterar" @@ -1867,7 +1879,7 @@ msgstr "Tema escuro" #~ msgid "Dark Theme" #~ msgstr "Modo Escuro" -#: src/screens/Signup/StepInfo/index.tsx:191 +#: src/screens/Signup/StepInfo/index.tsx:192 msgid "Date of birth" msgstr "Data de nascimento" @@ -2174,6 +2186,10 @@ msgstr "Baixe o Bluesky" msgid "Download CAR file" msgstr "Baixar arquivo CAR" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:591 +msgid "Download image" +msgstr "Baixar imagem" + #: src/view/com/composer/text-input/TextInput.web.tsx:269 msgid "Drop to add images" msgstr "Solte para adicionar imagens" @@ -2239,7 +2255,7 @@ msgctxt "action" msgid "Edit" msgstr "Editar" -#: src/view/com/util/UserAvatar.tsx:328 +#: src/view/com/util/UserAvatar.tsx:332 #: src/view/com/util/UserBanner.tsx:92 msgid "Edit avatar" msgstr "Editar avatar" @@ -3545,6 +3561,10 @@ msgstr "Crie uma conta no Bluesky" msgid "Join the conversation" msgstr "Participe da conversa" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:461 +msgid "Joined {0}" +msgstr "Juntou-se {0}" + #: src/screens/Onboarding/index.tsx:21 #: src/screens/Onboarding/state.ts:89 msgid "Journalism" @@ -4551,6 +4571,10 @@ msgstr "Opa!" msgid "Oh no! Something went wrong." msgstr "Opa! Algo deu errado." +#: src/components/dialogs/nuxs/TenMillion/index.tsx:171 +msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🩋" +msgstr "Ah, nĂŁo! NĂŁo conseguimos gerar uma imagem para vocĂȘ compartilhar. Fique tranquilo, estamos felizes que vocĂȘ esteja aqui 🩋" + #: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339 msgid "OK" msgstr "OK" @@ -4821,7 +4845,7 @@ msgid "Opens the threads preferences" msgstr "Abre as preferĂȘncias de threads" #: src/view/com/notifications/FeedItem.tsx:551 -#: src/view/com/util/UserAvatar.tsx:420 +#: src/view/com/util/UserAvatar.tsx:424 msgid "Opens this profile" msgstr "Abre este perfil" @@ -5410,7 +5434,7 @@ msgstr "Remover conta" msgid "Remove attachment" msgstr "" -#: src/view/com/util/UserAvatar.tsx:387 +#: src/view/com/util/UserAvatar.tsx:391 msgid "Remove Avatar" msgstr "Remover avatar" @@ -6153,7 +6177,7 @@ msgstr "Selecione quais idiomas vocĂȘ deseja ver nos seus feeds. Se nenhum for s msgid "Select your app language for the default text to display in the app." msgstr "Selecione o idioma do seu aplicativo" -#: src/screens/Signup/StepInfo/index.tsx:192 +#: src/screens/Signup/StepInfo/index.tsx:193 msgid "Select your date of birth" msgstr "Selecione sua data de nascimento" @@ -6321,6 +6345,7 @@ msgstr "Atividade sexual ou nudez erĂłtica." msgid "Sexually Suggestive" msgstr "Sexualmente Sugestivo" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:607 #: src/components/StarterPack/QrCodeDialog.tsx:177 #: src/screens/StarterPack/StarterPackScreen.tsx:411 #: src/screens/StarterPack/StarterPackScreen.tsx:582 @@ -6357,6 +6382,14 @@ msgstr "Compartilhar assim" msgid "Share feed" msgstr "Compartilhar feed" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:590 +msgid "Share image externally" +msgstr "Compartilhar imagem externamente" + +#: src/components/dialogs/nuxs/TenMillion/index.tsx:602 +msgid "Share image in post" +msgstr "Compartilhe a imagem na postagem" + #: src/components/StarterPack/ShareDialog.tsx:124 #: src/components/StarterPack/ShareDialog.tsx:131 #: src/screens/StarterPack/StarterPackScreen.tsx:586 @@ -6668,8 +6701,8 @@ msgstr "Algo deu errado. Por favor, tente novamente." msgid "Something went wrong!" msgstr "Algo deu errado!" -#: src/App.native.tsx:102 -#: src/App.web.tsx:83 +#: src/App.native.tsx:103 +#: src/App.web.tsx:84 msgid "Sorry! Your session expired. Please log in again." msgstr "Opa! Sua sessĂŁo expirou. Por favor, entre novamente." @@ -6912,6 +6945,10 @@ msgstr "Conte uma piada!" msgid "Tell us a little more" msgstr "Conte-nos um pouco mais" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:487 +msgid "Ten Million" +msgstr "Dez milhĂ”es" + #: src/view/shell/desktop/RightNav.tsx:90 msgid "Terms" msgstr "Termos" @@ -6949,6 +6986,10 @@ msgstr "Campo de entrada de texto" msgid "Thank you. Your report has been sent." msgstr "Obrigado. Sua denĂșncia foi enviada." +#: src/components/dialogs/nuxs/TenMillion/index.tsx:562 +msgid "Thanks for being one of our first 10 million users." +msgstr "Obrigado por ser um dos nossos primeiros 10 milhĂ”es de usuĂĄrios." + #: src/components/intents/VerifyEmailIntentDialog.tsx:74 msgid "Thanks, you have successfully verified your email address." msgstr "" @@ -7437,6 +7478,10 @@ msgstr "Para enviar vĂ­deos para o Bluesky, vocĂȘ deve primeiro verificar seu e- msgid "To whom would you like to send this report?" msgstr "Para quem vocĂȘ gostaria de enviar esta denĂșncia?" +#: src/components/dialogs/nuxs/TenMillion/index.tsx:566 +msgid "Together, we're rebuilding the social internet. We're glad you're here." +msgstr "Juntos, estamos reconstruindo a internet social. Estamos felizes que vocĂȘ esteja aqui." + #: src/components/dialogs/MutedWords.tsx:112 #~ msgid "Toggle between muted word options." #~ msgstr "Alternar entre opçÔes de uma palavra silenciada" @@ -7691,20 +7736,20 @@ msgstr "Enviar uma foto" msgid "Upload a text file to:" msgstr "Carregar um arquivo de texto para:" -#: src/view/com/util/UserAvatar.tsx:355 -#: src/view/com/util/UserAvatar.tsx:358 +#: src/view/com/util/UserAvatar.tsx:359 +#: src/view/com/util/UserAvatar.tsx:362 #: src/view/com/util/UserBanner.tsx:123 #: src/view/com/util/UserBanner.tsx:126 msgid "Upload from Camera" msgstr "Tirar uma foto" -#: src/view/com/util/UserAvatar.tsx:372 +#: src/view/com/util/UserAvatar.tsx:376 #: src/view/com/util/UserBanner.tsx:140 msgid "Upload from Files" msgstr "Carregar um arquivo" -#: src/view/com/util/UserAvatar.tsx:366 #: src/view/com/util/UserAvatar.tsx:370 +#: src/view/com/util/UserAvatar.tsx:374 #: src/view/com/util/UserBanner.tsx:134 #: src/view/com/util/UserBanner.tsx:138 msgid "Upload from Library" @@ -7904,7 +7949,7 @@ msgstr "Falha no processamento do vĂ­deo" msgid "Video Games" msgstr "Games" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:164 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:167 msgid "Video not found." msgstr "VĂ­deo nĂŁo encontrado." @@ -8572,11 +8617,11 @@ msgstr "Sua conta ainda nĂŁo tem idade suficiente para enviar vĂ­deos. Por favor msgid "Your account repository, containing all public data records, can be downloaded as a \"CAR\" file. This file does not include media embeds, such as images, or your private data, which must be fetched separately." msgstr "O repositĂłrio da sua conta, contendo todos os seus dados pĂșblicos, pode ser baixado como um arquivo \"CAR\". Este arquivo nĂŁo inclui imagens ou dados privados, estes devem ser exportados separadamente." -#: src/screens/Signup/StepInfo/index.tsx:180 +#: src/screens/Signup/StepInfo/index.tsx:181 msgid "Your birth date" msgstr "Sua data de nascimento" -#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:168 +#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:171 msgid "Your browser does not support the video format. Please try a different browser." msgstr "Seu navegador nĂŁo suporta o formato de vĂ­deo. Por favor, tente um navegador diferente." diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index ec30bab4a8..8588888b87 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({ variant="solid" color={'primary'} size="medium" - onPress={onPressNext} - disabled={!email}> + onPress={onPressNext}> Next diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 35b124b611..9a01c04990 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -60,7 +60,6 @@ export const LoginForm = ({ const {track} = useAnalytics() const t = useTheme() const [isProcessing, setIsProcessing] = useState(false) - const [isReady, setIsReady] = useState(false) const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false) const identifierValueRef = useRef(initialHandle || '') @@ -83,12 +82,18 @@ export const LoginForm = ({ Keyboard.dismiss() LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) setError('') - setIsProcessing(true) const identifier = identifierValueRef.current.toLowerCase().trim() const password = passwordValueRef.current const authFactorToken = authFactorTokenValueRef.current + if (!identifier || !password) { + setError(_(msg`Invalid username or password`)) + return + } + + setIsProcessing(true) + try { // try to guess the handle if the user just gave their own username let fullIdent = identifier @@ -157,22 +162,6 @@ export const LoginForm = ({ } } - const checkIsReady = () => { - if ( - !!serviceDescription && - !!identifierValueRef.current && - !!passwordValueRef.current - ) { - if (!isReady) { - setIsReady(true) - } - } else { - if (isReady) { - setIsReady(false) - } - } - } - return ( Sign in}> @@ -204,7 +193,6 @@ export const LoginForm = ({ defaultValue={initialHandle || ''} onChangeText={v => { identifierValueRef.current = v - checkIsReady() }} onSubmitEditing={() => { passwordRef.current?.focus() @@ -233,7 +221,6 @@ export const LoginForm = ({ clearButtonMode="while-editing" onChangeText={v => { passwordValueRef.current = v - checkIsReady() }} onSubmitEditing={onPressNext} blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing @@ -325,7 +312,7 @@ export const LoginForm = ({ Connecting... - ) : isReady ? ( + ) : ( - ) : undefined} + )} ) diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index c5cb1e9d9b..865967d37a 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -3,27 +3,16 @@ import zod from 'zod' import {BaseNux} from '#/state/queries/nuxs/types' export enum Nux { - One = 'one', - Two = 'two', + TenMillionDialog = 'TenMillionDialog', } export const nuxNames = new Set(Object.values(Nux)) -export type AppNux = - | BaseNux<{ - id: Nux.One - data: { - likes: number - } - }> - | BaseNux<{ - id: Nux.Two - data: undefined - }> +export type AppNux = BaseNux<{ + id: Nux.TenMillionDialog + data: undefined +}> -export const NuxSchemas = { - [Nux.One]: zod.object({ - likes: zod.number(), - }), - [Nux.Two]: undefined, +export const NuxSchemas: Record | undefined> = { + [Nux.TenMillionDialog]: undefined, } diff --git a/src/state/queries/nuxs/index.ts b/src/state/queries/nuxs/index.ts index 2945e67eb2..e183bcfad0 100644 --- a/src/state/queries/nuxs/index.ts +++ b/src/state/queries/nuxs/index.ts @@ -57,6 +57,7 @@ export function useUpsertNuxMutation() { const agent = useAgent() return useMutation({ + retry: 3, mutationFn: async (nux: AppNux) => { await agent.bskyAppUpsertNux(serializeAppNux(nux)) // triggers a refetch @@ -72,6 +73,7 @@ export function useRemoveNuxsMutation() { const agent = useAgent() return useMutation({ + retry: 3, mutationFn: async (ids: string[]) => { await agent.bskyAppRemoveNuxs(ids) // triggers a refetch diff --git a/src/state/queries/nuxs/types.ts b/src/state/queries/nuxs/types.ts index 5b79184704..2331582a1d 100644 --- a/src/state/queries/nuxs/types.ts +++ b/src/state/queries/nuxs/types.ts @@ -4,6 +4,4 @@ export type Data = Record | undefined export type BaseNux< T extends Pick & {data: Data}, -> = T & { - completed: boolean -} +> = Pick & T diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 6522d75a36..bc41fd3edf 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -1,4 +1,6 @@ /** * Device data that's specific to the device and does not vary based account */ -export type Device = {} +export type Device = { + lastNuxDialog: string | undefined +} diff --git a/src/view/com/composer/videos/SelectVideoBtn.tsx b/src/view/com/composer/videos/SelectVideoBtn.tsx index da67d781e0..2f2b4c3e7a 100644 --- a/src/view/com/composer/videos/SelectVideoBtn.tsx +++ b/src/view/com/composer/videos/SelectVideoBtn.tsx @@ -13,6 +13,8 @@ import {useVideoLibraryPermission} from '#/lib/hooks/usePermissions' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {BSKY_SERVICE} from 'lib/constants' +import {getHostnameFromUrl} from 'lib/strings/url-helpers' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {VideoClip_Stroke2_Corner0_Rounded as VideoClipIcon} from '#/components/icons/VideoClip' @@ -38,7 +40,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { return } - if (!currentAccount?.emailConfirmed) { + if ( + currentAccount && + !currentAccount.emailConfirmed && + getHostnameFromUrl(currentAccount.service) === + getHostnameFromUrl(BSKY_SERVICE) + ) { Keyboard.dismiss() control.open() } else { @@ -71,12 +78,12 @@ export function SelectVideoBtn({onSelectVideo, disabled, setError}: Props) { } } }, [ - onSelectVideo, requestVideoAccessIfNeeded, + currentAccount, + control, setError, _, - control, - currentAccount?.emailConfirmed, + onSelectVideo, ]) return ( diff --git a/src/view/com/composer/videos/VideoPreview.tsx b/src/view/com/composer/videos/VideoPreview.tsx index 60b467d62b..b1bfd6715e 100644 --- a/src/view/com/composer/videos/VideoPreview.tsx +++ b/src/view/com/composer/videos/VideoPreview.tsx @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/no-shadow */ import React from 'react' import {View} from 'react-native' import {ImagePickerAsset} from 'expo-image-picker' -import {useVideoPlayer, VideoView} from 'expo-video' +import {BlueskyVideoView} from '@haileyok/bluesky-video' import {CompressedVideo} from '#/lib/media/video/types' import {clamp} from '#/lib/numbers' @@ -22,15 +21,8 @@ export function VideoPreview({ clear: () => void }) { const t = useTheme() + const playerRef = React.useRef(null) const autoplayDisabled = useAutoplayDisabled() - const player = useVideoPlayer(video.uri, player => { - player.loop = true - player.muted = true - if (!autoplayDisabled) { - player.play() - } - }) - let aspectRatio = asset.width / asset.height if (isNaN(aspectRatio)) { @@ -50,12 +42,12 @@ export function VideoPreview({ t.atoms.border_contrast_low, {backgroundColor: 'black'}, ]}> - {autoplayDisabled && ( diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 79dd2f4917..f9aeae1a86 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -1,6 +1,7 @@ import React, {memo} from 'react' import {FlatListProps, RefreshControl, ViewToken} from 'react-native' import {runOnJS, useSharedValue} from 'react-native-reanimated' +import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {usePalette} from '#/lib/hooks/usePalette' @@ -8,7 +9,6 @@ import {useScrollHandlers} from '#/lib/ScrollContext' import {useDedupe} from 'lib/hooks/useDedupe' import {addStyle} from 'lib/styles' import {isIOS} from 'platform/detection' -import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {FlatList_INTERNAL} from './Views' export type ListMethods = FlatList_INTERNAL @@ -69,7 +69,7 @@ function ListImpl( onBeginDragFromContext?.(e, ctx) }, onEndDrag(e, ctx) { - runOnJS(updateActiveViewAsync)() + runOnJS(updateActiveVideoViewAsync)() onEndDragFromContext?.(e, ctx) }, onScroll(e, ctx) { @@ -84,13 +84,13 @@ function ListImpl( } if (isIOS) { - runOnJS(dedupe)(updateActiveViewAsync) + runOnJS(dedupe)(updateActiveVideoViewAsync) } }, // Note: adding onMomentumBegin here makes simulator scroll // lag on Android. So either don't add it, or figure out why. onMomentumEnd(e, ctx) { - runOnJS(updateActiveViewAsync)() + runOnJS(updateActiveVideoViewAsync)() onMomentumEndFromContext?.(e, ctx) }, }) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 66c9708bfc..4ad37ebd75 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -19,7 +19,7 @@ import {colors} from 'lib/styles' import {isAndroid, isNative, isWeb} from 'platform/detection' import {precacheProfile} from 'state/queries/profile' import {HighPriorityImage} from 'view/com/util/images/Image' -import {tokens, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' import { Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled, Camera_Stroke2_Corner0_Rounded as Camera, @@ -27,6 +27,7 @@ import { import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive' import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Link} from '#/components/Link' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Menu from '#/components/Menu' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {openCamera, openCropper, openPicker} from '../../../lib/media/picker' @@ -43,6 +44,7 @@ interface BaseUserAvatarProps { interface UserAvatarProps extends BaseUserAvatarProps { moderation?: ModerationUI usePlainRNImage?: boolean + onLoad?: () => void } interface EditableUserAvatarProps extends BaseUserAvatarProps { @@ -174,6 +176,7 @@ let UserAvatar = ({ avatar, moderation, usePlainRNImage = false, + onLoad, }: UserAvatarProps): React.ReactNode => { const pal = usePalette('default') const backgroundColor = pal.colors.backgroundLight @@ -224,6 +227,7 @@ let UserAvatar = ({ uri: hackModifyThumbnailPath(avatar, size < 90), }} blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} + onLoad={onLoad} /> ) : ( )} + {alert} ) : ( diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 932a182803..9abbe2875f 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -11,6 +11,7 @@ import {isNative} from '#/platform/detection' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' export function useImageAspectRatio({ @@ -140,6 +141,7 @@ export function AutoSizedImage({ accessibilityLabel={image.alt} accessibilityHint="" /> + {(hasAlt || isCropped) && !hideBadge ? ( void @@ -46,7 +47,7 @@ export const GalleryItem: FC = ({ onLongPress={onLongPress ? () => onLongPress(index) : undefined} style={[ a.flex_1, - a.rounded_xs, + a.rounded_sm, a.overflow_hidden, t.atoms.bg_contrast_25, imageStyle, @@ -62,6 +63,7 @@ export const GalleryItem: FC = ({ accessibilityHint="" accessibilityIgnoresInvertColors /> + {hasAlt && !hideBadges ? ( void - player: VideoPlayer -} | null>(null) - -export function Provider({children}: {children: React.ReactNode}) { - if (!isNative) { - throw new Error('ActiveVideoProvider may only be used on native.') - } - - const [activeSource, setActiveSource] = React.useState('') - const [activeViewId, setActiveViewId] = React.useState() - - const player = useVideoPlayer(activeSource, p => { - p.muted = true - p.loop = true - // We want to immediately call `play` so we get the loading state - p.play() - }) - - const setActiveSourceOuter = (src: string | null, viewId: string | null) => { - // HACK - // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually - // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to - // apply it there. - if (src === activeSource && isAndroid) { - setActiveSource('') - setTimeout(() => { - setActiveSource(src ? src : '') - }, 100) - } else { - setActiveSource(src ? src : '') - } - setActiveViewId(viewId ? viewId : '') - } - - return ( - - {children} - - ) -} - -export function useActiveVideoNative() { - const context = React.useContext(Context) - if (!context) { - throw new Error( - 'useActiveVideoNative must be used within a ActiveVideoNativeProvider', - ) - } - return context -} diff --git a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx index b2720752ca..1f966d7107 100644 --- a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx @@ -5,19 +5,21 @@ import { LayoutChangeEvent, Pressable, StyleSheet, - View, } from 'react-native' import {Image, ImageLoadEventData} from 'expo-image' import {AppBskyEmbedExternal} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {EmbedPlayerParams, getGifDims} from '#/lib/strings/embed-player' import {isIOS, isNative, isWeb} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' +import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' +import {Fill} from '#/components/Fill' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' +import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' export function ExternalGifEmbed({ link, @@ -26,6 +28,7 @@ export function ExternalGifEmbed({ link: AppBskyEmbedExternal.ViewExternal params: EmbedPlayerParams }) { + const t = useTheme() const externalEmbedsPrefs = useExternalEmbedsPrefs() const {_} = useLingui() @@ -113,26 +116,19 @@ export function ExternalGifEmbed({ - {(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay - - - {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active - - ) : ( - // Activity indicator while gif loads - - )} - - - )} + + {(!isPrefetched || !isAnimating) && ( + + + + {!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active + + ) : ( + // Activity indicator while gif loads + + )} + + )} + ) @@ -171,7 +196,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: 'rgba(0,0,0,0.5)', }, overlayLayer: { zIndex: 2, diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index cc742c8c01..0bd65de8b2 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -21,6 +21,7 @@ import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed' import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed' import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed' import {atoms as a, useTheme} from '#/alf' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '../text/Text' export const ExternalLinkEmbed = ({ @@ -36,6 +37,7 @@ export const ExternalLinkEmbed = ({ }) => { const {_} = useLingui() const pal = usePalette('default') + const t = useTheme() const {isMobile} = useWebMediaQueries() const externalEmbedPrefs = useExternalEmbedsPrefs() @@ -60,19 +62,30 @@ export const ExternalLinkEmbed = ({ {imageUri && !embedPlayerParams ? ( - + + + + ) : undefined} {embedPlayerParams?.isGif ? ( @@ -81,11 +94,18 @@ export const ExternalLinkEmbed = ({ ) : undefined} children: React.ReactNode }) { - const t = useTheme() - const onShareExternal = useCallback(() => { if (link.uri && isNative) { shareUrl(link.uri) @@ -137,14 +155,7 @@ function LinkWrapper({ asAnchor anchorNoUnderline href={link.uri} - style={[ - a.flex_1, - a.border, - a.rounded_sm, - t.atoms.border_contrast_medium, - style, - ]} - hoverStyle={t.atoms.border_contrast_high} + style={[a.flex_1, a.rounded_sm, style]} onBeforePress={onOpen} onLongPress={onShareExternal}> {children} diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx index 111867fc61..a1e509ef47 100644 --- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx @@ -25,9 +25,11 @@ import {NavigationProp} from '#/lib/routes/types' import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player' import {isNative} from '#/platform/detection' import {useExternalEmbedsPrefs} from '#/state/preferences' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' +import {Fill} from '#/components/Fill' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {EventStopper} from '../EventStopper' @@ -106,6 +108,16 @@ function Player({ style={styles.webview} setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads) /> + + ) } @@ -118,6 +130,7 @@ export function ExternalPlayer({ link: AppBskyEmbedExternal.ViewExternal params: EmbedPlayerParams }) { + const t = useTheme() const navigation = useNavigation() const insets = useSafeAreaInsets() const windowDims = useWindowDimensions() @@ -211,13 +224,38 @@ export function ExternalPlayer({ onAccept={onAcceptConsent} /> - + {link.thumb && (!isPlayerActive || isLoading) && ( - + <> + + + + )} }) { + const t = useTheme() const {_} = useLingui() const autoplayDisabled = useAutoplayDisabled() @@ -138,6 +139,17 @@ export function GifEmbed({ accessibilityHint={_(msg`Animated GIF`)} accessibilityLabel={parsedAlt.alt} /> + {!playerState.isPlaying && ( + + )} + {!hideAlt && parsedAlt.isPreferred && } diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 87a6edebde..d50119025f 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -33,7 +33,7 @@ import {InfoCircleIcon} from 'lib/icons' import {makeProfileLink} from 'lib/routes/links' import {precacheProfile} from 'state/queries/profile' import {ComposerOptsQuote} from 'state/shell/composer' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme} from '#/alf' import {RichText} from '#/components/RichText' import {ContentHider} from '../../../../components/moderation/ContentHider' import {PostAlerts} from '../../../../components/moderation/PostAlerts' @@ -56,6 +56,7 @@ export function MaybeQuoteEmbed({ allowNestedQuotes?: boolean viewContext?: QuoteEmbedViewContext }) { + const t = useTheme() const pal = usePalette('default') const {currentAccount} = useSession() if ( @@ -75,7 +76,8 @@ export function MaybeQuoteEmbed({ ) } else if (AppBskyEmbedRecord.isViewBlocked(embed.record)) { return ( - + Blocked @@ -84,7 +86,8 @@ export function MaybeQuoteEmbed({ ) } else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) { return ( - + Deleted @@ -96,7 +99,8 @@ export function MaybeQuoteEmbed({ ? embed.record.uri.includes(currentAccount.did) : false return ( - + {isViewerOwner ? ( @@ -169,6 +173,7 @@ export function QuoteEmbed({ allowNestedQuotes?: boolean viewContext?: QuoteEmbedViewContext }) { + const t = useTheme() const queryClient = useQueryClient() const pal = usePalette('default') const itemUrip = new AtUri(quote.uri) @@ -214,7 +219,7 @@ export function QuoteEmbed({ return ( void}>(null) - const [playerStatus, setPlayerStatus] = useState< - VideoPlayerStatus | 'paused' - >('paused') - const [isMuted, setIsMuted] = useState(player.muted) - const [isFullscreen, setIsFullscreen] = React.useState(false) - const [timeRemaining, setTimeRemaining] = React.useState(0) - const isWithinMessage = useIsWithinMessage() - const disableAutoplay = useAutoplayDisabled() || isWithinMessage - const isActive = embed.playlist === activeSource && activeViewId === viewId - // There are some different loading states that we should pay attention to and show a spinner for - const isLoading = - isActive && - (playerStatus === 'waitingToPlayAtSpecifiedRate' || - playerStatus === 'loading') - // This happens whenever the visibility view decides that another video should start playing - const showOverlay = !isActive || isLoading || playerStatus === 'paused' + const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>( + 'pending', + ) + const [isLoading, setIsLoading] = React.useState(false) + const [isActive, setIsActive] = React.useState(false) + const showSpinner = useThrottledValue(isActive && isLoading, 100) - // send error up to error boundary - const [error, setError] = useState(null) - if (error) { - throw error - } + const showOverlay = + !isActive || + isLoading || + (status === 'paused' && !isActive) || + status === 'pending' - useEffect(() => { - if (isActive) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const volumeSub = player.addListener('volumeChange', ({isMuted}) => { - setIsMuted(isMuted) - }) - const timeSub = player.addListener( - 'timeRemainingChange', - secondsRemaining => { - setTimeRemaining(secondsRemaining) - }, - ) - const statusSub = player.addListener( - 'statusChange', - (status, oldStatus, playerError) => { - setPlayerStatus(status) - if (status === 'error') { - setError(playerError ?? new Error('Unknown player error')) - } - if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') { - player.play() - } - }, - ) - return () => { - volumeSub.remove() - timeSub.remove() - statusSub.remove() - } + React.useEffect(() => { + if (!isActive && status !== 'pending') { + setStatus('pending') } - }, [player, isActive, disableAutoplay]) - - // The source might already be active (for example, if you are scrolling a list of quotes and its all the same - // video). In those cases, just start playing. Otherwise, setting the active source will result in the video - // start playback immediately - const startPlaying = (ignoreAutoplayPreference: boolean) => { - if (disableAutoplay && !ignoreAutoplayPreference) { - return - } - - if (isActive) { - player.play() - } else { - setActiveSource(embed.playlist, viewId) - } - } - - const onVisibilityStatusChange = (isVisible: boolean) => { - // When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change - // events - if (isFullscreen) { - return - } - if (isVisible) { - startPlaying(false) - } else { - // Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted - // until it gets replaced by another video - if (disableAutoplay) { - setActiveSource(null, null) - } else { - player.muted = true - if (player.playing) { - player.pause() - } - } - } - } + }, [isActive, status]) return ( - - {isActive ? ( - - ) : null} + <> + - + ) } diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx index be3f907112..66e1df50dc 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {StyleProp, ViewStyle} from 'react-native' import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated' import {atoms as a, native, useTheme} from '#/alf' @@ -8,7 +9,13 @@ import {Text} from '#/components/Typography' * Absolutely positioned time indicator showing how many seconds are remaining * Time is in seconds */ -export function TimeIndicator({time}: {time: number}) { +export function TimeIndicator({ + time, + style, +}: { + time: number + style?: StyleProp +}) { const t = useTheme() if (isNaN(time)) { @@ -22,18 +29,20 @@ export function TimeIndicator({time}: {time: number}) { void - timeRemaining: number - isMuted: boolean -}) { - const {_} = useLingui() - const {player} = useActiveVideoNative() - const ref = useRef(null) +export const VideoEmbedInnerNative = React.forwardRef( + function VideoEmbedInnerNative( + { + embed, + setStatus, + setIsLoading, + setIsActive, + }: { + embed: AppBskyEmbedVideo.View + setStatus: (status: 'playing' | 'paused') => void + setIsLoading: (isLoading: boolean) => void + setIsActive: (isActive: boolean) => void + }, + ref: React.Ref<{togglePlayback: () => void}>, + ) { + const {_} = useLingui() + const videoRef = useRef(null) + const autoplayDisabled = useAutoplayDisabled() + const isWithinMessage = useIsWithinMessage() + const {muted, setMuted} = useVideoVolumeState() - const enterFullscreen = useCallback(() => { - ref.current?.enterFullscreen() - }, []) + const [isPlaying, setIsPlaying] = React.useState(false) + const [timeRemaining, setTimeRemaining] = React.useState(0) + const [error, setError] = React.useState() - let aspectRatio = 16 / 9 + React.useImperativeHandle(ref, () => ({ + togglePlayback: () => { + videoRef.current?.togglePlayback() + }, + })) - if (embed.aspectRatio) { - const {width, height} = embed.aspectRatio - aspectRatio = width / height - aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) - } + if (error) { + throw new Error(error) + } - return ( - - { - PlatformInfo.setAudioCategory(AudioCategory.Playback) - PlatformInfo.setAudioActive(true) - player.muted = false - setIsFullscreen(true) - if (isAndroid) { - player.play() + let aspectRatio = 16 / 9 + + if (embed.aspectRatio) { + const {width, height} = embed.aspectRatio + aspectRatio = width / height + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) + } + + return ( + + { + setIsActive(e.nativeEvent.isActive) + }} + onLoadingChange={e => { + setIsLoading(e.nativeEvent.isLoading) + }} + onMutedChange={e => { + setMuted(e.nativeEvent.isMuted) + }} + onStatusChange={e => { + setStatus(e.nativeEvent.status) + setIsPlaying(e.nativeEvent.status === 'playing') + }} + onTimeRemainingChange={e => { + setTimeRemaining(e.nativeEvent.timeRemaining) + }} + onError={e => { + setError(e.nativeEvent.error) + }} + ref={videoRef} + accessibilityLabel={ + embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) } - }} - onFullscreenExit={() => { - PlatformInfo.setAudioCategory(AudioCategory.Ambient) - PlatformInfo.setAudioActive(false) - player.muted = true - player.playbackRate = 1 - setIsFullscreen(false) - }} - accessibilityLabel={ - embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) - } - accessibilityHint="" - /> - - - ) -} + accessibilityHint="" + /> + { + videoRef.current?.enterFullscreen() + }} + toggleMuted={() => { + videoRef.current?.toggleMuted() + }} + togglePlayback={() => { + videoRef.current?.togglePlayback() + }} + isPlaying={isPlaying} + timeRemaining={timeRemaining} + /> + + + ) + }, +) function VideoControls({ - player, enterFullscreen, + toggleMuted, + togglePlayback, timeRemaining, - isMuted, + isPlaying, }: { - player: VideoPlayer enterFullscreen: () => void + toggleMuted: () => void + togglePlayback: () => void timeRemaining: number - isMuted: boolean + isPlaying: boolean }) { const {_} = useLingui() const t = useTheme() - - const onPressFullscreen = useCallback(() => { - switch (player.status) { - case 'idle': - case 'loading': - case 'readyToPlay': { - if (!player.playing) player.play() - enterFullscreen() - break - } - case 'error': { - player.replay() - break - } - } - }, [player, enterFullscreen]) - - const toggleMuted = useCallback(() => { - const muted = !player.muted - // We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when - // the video is muted, and vice versa. - const mix = !muted - const category = muted ? AudioCategory.Ambient : AudioCategory.Playback - - PlatformInfo.setAudioCategory(category) - PlatformInfo.setAudioActive(mix) - player.muted = muted - }, [player]) + const {muted} = useVideoVolumeState() // show countdown when: // 1. timeRemaining is a number - was seeing NaNs @@ -138,44 +138,80 @@ function VideoControls({ return ( - {showTime && } - - - {isMuted ? ( - - ) : ( - - )} - - + + {isPlaying ? ( + + ) : ( + + )} + + {showTime && } + + + {muted ? ( + + ) : ( + + )} + ) } + +function ControlButton({ + onPress, + children, + label, + accessibilityHint, + style, +}: { + onPress: () => void + children: React.ReactNode + label: string + accessibilityHint: string + style?: StyleProp +}) { + return ( + + + {children} + + + ) +} diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 441be75724..5f569a818b 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -4,6 +4,7 @@ import {AppBskyEmbedVideo} from '@atproto/api' import Hls from 'hls.js' import {atoms as a} from '#/alf' +import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Controls} from './VideoWebControls' export function VideoEmbedInnerWeb({ @@ -119,6 +120,7 @@ export function VideoEmbedInnerWeb({ fullscreenRef={containerRef} hasSubtitleTrack={hasSubtitleTrack} /> + ) diff --git a/src/view/com/util/post-embeds/VideoVolumeContext.tsx b/src/view/com/util/post-embeds/VideoVolumeContext.tsx new file mode 100644 index 0000000000..cccb93ba8b --- /dev/null +++ b/src/view/com/util/post-embeds/VideoVolumeContext.tsx @@ -0,0 +1,32 @@ +import React from 'react' + +const Context = React.createContext( + {} as { + muted: boolean + setMuted: (muted: boolean) => void + }, +) + +export function Provider({children}: {children: React.ReactNode}) { + const [muted, setMuted] = React.useState(true) + + const value = React.useMemo( + () => ({ + muted, + setMuted, + }), + [muted, setMuted], + ) + + return {children} +} + +export function useVideoVolumeState() { + const context = React.useContext(Context) + if (!context) { + throw new Error( + 'useVideoVolumeState must be used within a VideoVolumeProvider', + ) + } + return context +} diff --git a/src/view/icons/Logomark.tsx b/src/view/icons/Logomark.tsx new file mode 100644 index 0000000000..5715a1a404 --- /dev/null +++ b/src/view/icons/Logomark.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import Svg, {Path, PathProps, SvgProps} from 'react-native-svg' + +import {usePalette} from '#/lib/hooks/usePalette' + +const ratio = 54 / 61 + +export function Logomark({ + fill, + ...rest +}: {fill?: PathProps['fill']} & SvgProps) { + const pal = usePalette('default') + // @ts-ignore it's fiiiiine + const size = parseInt(rest.width || 32) + + return ( + + + + ) +} diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx index 42696139e0..ee1ed66226 100644 --- a/src/view/shell/Composer.web.tsx +++ b/src/view/shell/Composer.web.tsx @@ -63,6 +63,7 @@ export function Composer({}: {winHeight: number}) { mention={state.mention} openEmojiPicker={onOpenPicker} text={state.text} + imageUris={state.imageUris} /> diff --git a/yarn.lock b/yarn.lock index b2e389aa13..16cfb34067 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4104,6 +4104,11 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@haileyok/bluesky-video@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.4.tgz#76acad0dffb9c80745bb752577be23cb566e4562" + integrity sha512-ggpk6E6U3giT+tmTc4GPraViA3ssnP32/Bty61UbZ3LiCQuc694LX+AOt01SfQ0B0fyd63J9DtT5rfaEJyjuzg== + "@hapi/accept@^6.0.3": version "6.0.3" resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-6.0.3.tgz#eef0800a4f89cd969da8e5d0311dc877c37279ab" @@ -12415,10 +12420,6 @@ expo-updates@~0.25.14: ignore "^5.3.1" resolve-from "^5.0.0" -"expo-video@https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz": - version "1.2.4" - resolved "https://github.com/bluesky-social/expo/raw/expo-video-1.2.4-patch/packages/expo-video/expo-video-v1.2.4-2.tgz#4127dd5cea5fdf7ab745104c73b8ecf5506f5d34" - expo-web-browser@~13.0.3: version "13.0.3" resolved "https://registry.yarnpkg.com/expo-web-browser/-/expo-web-browser-13.0.3.tgz#dceb05dbc187b498ca937b02adf385b0232a4e92"