Merge remote-tracking branch 'origin/main' into eric/new-post-keyboard-shortcut
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 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" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 378 B |
+2
-2
@@ -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",
|
||||
|
||||
+7
-8
@@ -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() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<RootSiblingParent>
|
||||
<VideoVolumeProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
@@ -131,6 +131,7 @@ function InnerApp() {
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
@@ -145,8 +146,8 @@ function InnerApp() {
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
</Splash>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
@@ -157,8 +158,6 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||
PlatformInfo.setAudioActive(false)
|
||||
initPersistedState().then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
|
||||
+40
-35
@@ -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() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
<VideoVolumeProvider>
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<HiddenRepliesProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</HiddenRepliesProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
</VideoVolumeProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
+15
-3
@@ -18,9 +18,17 @@ export * from '#/alf/util/themeSelector'
|
||||
export const Context = React.createContext<{
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
themes: ReturnType<typeof createThemes>
|
||||
}>({
|
||||
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({
|
||||
<Context.Provider
|
||||
value={React.useMemo(
|
||||
() => ({
|
||||
themes,
|
||||
themeName: themeName,
|
||||
theme: theme,
|
||||
}),
|
||||
[theme, themeName],
|
||||
[theme, themeName, themes],
|
||||
)}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -256,7 +256,7 @@ export const ScrollableInner = React.forwardRef<
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
flatten(style),
|
||||
style,
|
||||
]}
|
||||
contentContainerStyle={a.pb_4xl}
|
||||
ref={ref}>
|
||||
|
||||
@@ -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 <View style={[a.absolute, a.inset_0, style]}>{children}</View>
|
||||
}
|
||||
@@ -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 (
|
||||
<Fill
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
opaque
|
||||
? [t.atoms.border_contrast_low]
|
||||
: [
|
||||
isLight
|
||||
? t.atoms.border_contrast_low
|
||||
: t.atoms.border_contrast_high,
|
||||
{opacity: 0.6},
|
||||
],
|
||||
{
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</Fill>
|
||||
)
|
||||
}
|
||||
@@ -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=""
|
||||
/>
|
||||
<MediaInsetBorder style={[a.rounded_xs]} />
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
paddingTop: '80%',
|
||||
},
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function TenMillion() {
|
||||
const agent = useAgent()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const [userNumber, setUserNumber] = React.useState<number>(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 ? <TenMillionInner userNumber={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<string | null>(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<ViewShot>(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 : (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.overflow_hidden,
|
||||
DEBUG
|
||||
? {
|
||||
width: 600,
|
||||
height: 600 * RATIO,
|
||||
}
|
||||
: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
]}>
|
||||
<View style={{width: 600}}>
|
||||
<ThemeProvider theme="light">
|
||||
<Frame>
|
||||
<ViewShot
|
||||
ref={imageRef}
|
||||
options={{width: WIDTH, height: HEIGHT}}
|
||||
style={[a.absolute, a.inset_0]}>
|
||||
<View
|
||||
onLayout={onCanvasReady}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
top: -1,
|
||||
bottom: -1,
|
||||
left: -1,
|
||||
right: -1,
|
||||
paddingVertical: 48,
|
||||
paddingHorizontal: 48,
|
||||
},
|
||||
]}>
|
||||
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_md,
|
||||
{
|
||||
backgroundColor: 'white',
|
||||
shadowRadius: 32,
|
||||
shadowOpacity: 0.1,
|
||||
elevation: 24,
|
||||
shadowColor: tokens.gradients.bonfire.values[0][1],
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.px_xl,
|
||||
a.py_xl,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
]}>
|
||||
<Logomark fill={t.palette.primary_500} width={36} />
|
||||
</View>
|
||||
|
||||
{/* Centered content */}
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
paddingBottom: isNative ? 0 : 24,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
a.pb_sm,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
Celebrating {formatCount(i18n, 10000000)} users
|
||||
</Trans>{' '}
|
||||
🎉
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_start]}>
|
||||
<Text
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
color: t.palette.primary_500,
|
||||
fontSize: 32,
|
||||
fontWeight: '900',
|
||||
width: 32,
|
||||
top: isNative ? -10 : 0,
|
||||
left: 0,
|
||||
transform: [
|
||||
{
|
||||
translateX: -16,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
#
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.relative,
|
||||
a.text_center,
|
||||
{
|
||||
fontStyle: 'italic',
|
||||
fontSize: getFontSize(userNumber),
|
||||
lineHeight: getFontSize(userNumber),
|
||||
fontWeight: '900',
|
||||
letterSpacing: -2,
|
||||
},
|
||||
]}>
|
||||
{i18n.number(userNumber)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{Badge && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
width: 64,
|
||||
height: 64,
|
||||
top: isNative ? 75 : 85,
|
||||
right: '5%',
|
||||
transform: [
|
||||
{
|
||||
rotate: '8deg',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}>
|
||||
<Badge fill={t.palette.primary_500} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* End centered content */}
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.px_xl,
|
||||
a.py_xl,
|
||||
{
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{/*
|
||||
<UserAvatar
|
||||
size={36}
|
||||
avatar={profile.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
onLoad={onCanvasReady}
|
||||
/>
|
||||
*/}
|
||||
<View style={[a.gap_2xs, a.flex_1]}>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_bold,
|
||||
a.leading_tight,
|
||||
{maxWidth: '60%'},
|
||||
]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName ||
|
||||
sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
</Text>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_semibold,
|
||||
a.leading_snug,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
</Text>
|
||||
|
||||
{profile.createdAt && (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="head"
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.font_semibold,
|
||||
a.leading_snug,
|
||||
a.text_right,
|
||||
lightTheme.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Trans>
|
||||
Joined{' '}
|
||||
{i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ViewShot>
|
||||
</Frame>
|
||||
</ThemeProvider>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} onClose={onClose}>
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Ten Million`)}
|
||||
style={[
|
||||
{
|
||||
padding: 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
isNative && {
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
},
|
||||
]}>
|
||||
<Frame>
|
||||
<View
|
||||
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
|
||||
<GradientFill gradient={tokens.gradients.bonfire} />
|
||||
{error ? (
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
a.pb_md,
|
||||
{
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
(╯°□°)╯︵ ┻━┻
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_xl,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
{
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
) : isLoadingData || isLoadingImage ? (
|
||||
<Loader size="xl" fill="white" />
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
style={[a.w_full, a.h_full]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={{uri}}
|
||||
style={[a.w_full, a.h_full]}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</Frame>
|
||||
|
||||
{canvas}
|
||||
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_5xl,
|
||||
a.leading_tight,
|
||||
a.pb_lg,
|
||||
{
|
||||
fontWeight: '900',
|
||||
},
|
||||
]}>
|
||||
<Trans>Thanks for being one of our first 10 million users.</Trans>
|
||||
</Text>
|
||||
|
||||
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
|
||||
<Trans>
|
||||
Together, we're rebuilding the social internet. We're glad
|
||||
you're here.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_end,
|
||||
a.gap_md,
|
||||
a.pt_xl,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
label={
|
||||
isNative && isIOS
|
||||
? _(msg`Share image externally`)
|
||||
: _(msg`Download image`)
|
||||
}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
onPress={
|
||||
isNative
|
||||
? isIOS
|
||||
? onNativeShare
|
||||
: onNativeDownload
|
||||
: onWebDownload
|
||||
}>
|
||||
<ButtonIcon icon={isNative && isIOS ? Share : Download} />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
label={_(msg`Share image in post`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onPress={sharePost}>
|
||||
<ButtonText>{_(msg`Share`)}</ButtonText>
|
||||
<ButtonIcon position="right" icon={ImageIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof useGate>}): 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<Context>({
|
||||
activeNux: undefined,
|
||||
dismissActiveNux: () => {},
|
||||
})
|
||||
|
||||
export function useNuxDialogContext() {
|
||||
return React.useContext(Context)
|
||||
}
|
||||
|
||||
export function NuxDialogs() {
|
||||
const {hasSession} = useSession()
|
||||
return hasSession ? <Inner /> : null
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
const gate = useGate()
|
||||
const {nuxs} = useNuxs()
|
||||
const [snoozed, setSnoozed] = React.useState(() => {
|
||||
return isSnoozed()
|
||||
})
|
||||
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
|
||||
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 (
|
||||
<Context.Provider value={ctx}>
|
||||
{activeNux === Nux.TenMillionDialog && <TenMillion />}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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',
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -225,4 +225,8 @@ export type LogEvents = {
|
||||
'test:gate1:sometimes': {}
|
||||
'test:gate2:always': {}
|
||||
'test:gate2:sometimes': {}
|
||||
|
||||
'tmd:share': {}
|
||||
'tmd:download': {}
|
||||
'tmd:post': {}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -115,7 +115,7 @@ msgstr "<0><1>テキストとタグ</1>中の</0>{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 "メールでの2要素認証を無効化"
|
||||
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/></2></0>"
|
||||
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 "1つもしくは複数の画像に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 "メールでの2要素認証を無効にするには、メールアド
|
||||
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 "返信を公開しました"
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
|
||||
@@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({
|
||||
variant="solid"
|
||||
color={'primary'}
|
||||
size="medium"
|
||||
onPress={onPressNext}
|
||||
disabled={!email}>
|
||||
onPress={onPressNext}>
|
||||
<ButtonText>
|
||||
<Trans>Next</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -60,7 +60,6 @@ export const LoginForm = ({
|
||||
const {track} = useAnalytics()
|
||||
const t = useTheme()
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
||||
const [isReady, setIsReady] = useState<boolean>(false)
|
||||
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
|
||||
useState<boolean>(false)
|
||||
const identifierValueRef = useRef<string>(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 (
|
||||
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
|
||||
<View>
|
||||
@@ -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 = ({
|
||||
<Trans>Connecting...</Trans>
|
||||
</Text>
|
||||
</>
|
||||
) : isReady ? (
|
||||
) : (
|
||||
<Button
|
||||
testID="loginNextButton"
|
||||
label={_(msg`Next`)}
|
||||
@@ -339,7 +326,7 @@ export const LoginForm = ({
|
||||
</ButtonText>
|
||||
{isProcessing && <ButtonIcon icon={Loader} />}
|
||||
</Button>
|
||||
) : undefined}
|
||||
)}
|
||||
</View>
|
||||
</FormContainer>
|
||||
)
|
||||
|
||||
@@ -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<Nux, zod.ZodObject<any> | undefined> = {
|
||||
[Nux.TenMillionDialog]: undefined,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,4 @@ export type Data = Record<string, unknown> | undefined
|
||||
|
||||
export type BaseNux<
|
||||
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
|
||||
> = T & {
|
||||
completed: boolean
|
||||
}
|
||||
> = Pick<AppBskyActorDefs.Nux, 'id' | 'completed' | 'expiresAt'> & T
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<BlueskyVideoView>(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'},
|
||||
]}>
|
||||
<VideoView
|
||||
player={player}
|
||||
style={a.flex_1}
|
||||
allowsPictureInPicture={false}
|
||||
nativeControls={false}
|
||||
contentFit="contain"
|
||||
<BlueskyVideoView
|
||||
url={video.uri}
|
||||
autoplay={autoplayDisabled}
|
||||
beginMuted={true}
|
||||
forceTakeover={true}
|
||||
ref={playerRef}
|
||||
/>
|
||||
<ExternalEmbedRemoveBtn onRemove={clear} />
|
||||
{autoplayDisabled && (
|
||||
|
||||
@@ -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<ItemT>(
|
||||
onBeginDragFromContext?.(e, ctx)
|
||||
},
|
||||
onEndDrag(e, ctx) {
|
||||
runOnJS(updateActiveViewAsync)()
|
||||
runOnJS(updateActiveVideoViewAsync)()
|
||||
onEndDragFromContext?.(e, ctx)
|
||||
},
|
||||
onScroll(e, ctx) {
|
||||
@@ -84,13 +84,13 @@ function ListImpl<ItemT>(
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
<HighPriorityImage
|
||||
@@ -234,8 +238,10 @@ let UserAvatar = ({
|
||||
uri: hackModifyThumbnailPath(avatar, size < 90),
|
||||
}}
|
||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
)}
|
||||
<MediaInsetBorder style={[a.rounded_full]} />
|
||||
{alert}
|
||||
</View>
|
||||
) : (
|
||||
|
||||
@@ -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=""
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
|
||||
{(hasAlt || isCropped) && !hideBadge ? (
|
||||
<View
|
||||
|
||||
@@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type EventFunction = (index: number) => void
|
||||
@@ -46,7 +47,7 @@ export const GalleryItem: FC<GalleryItemProps> = ({
|
||||
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<GalleryItemProps> = ({
|
||||
accessibilityHint=""
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</Pressable>
|
||||
{hasAlt && !hideBadges ? (
|
||||
<View
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from 'react'
|
||||
import {useVideoPlayer, VideoPlayer} from 'expo-video'
|
||||
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
|
||||
const Context = React.createContext<{
|
||||
activeSource: string
|
||||
activeViewId: string | undefined
|
||||
setActiveSource: (src: string | null, viewId: string | null) => 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<string>()
|
||||
|
||||
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 (
|
||||
<Context.Provider
|
||||
value={{
|
||||
activeSource,
|
||||
setActiveSource: setActiveSourceOuter,
|
||||
activeViewId,
|
||||
player,
|
||||
}}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useActiveVideoNative() {
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useActiveVideoNative must be used within a ActiveVideoNativeProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -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({
|
||||
<Pressable
|
||||
style={[
|
||||
{height: imageDims.height},
|
||||
styles.topRadius,
|
||||
styles.gifContainer,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
onPress={onPlayPress}
|
||||
onLayout={onLayout}
|
||||
accessibilityRole="button"
|
||||
accessibilityHint={_(msg`Plays the GIF`)}
|
||||
accessibilityLabel={_(msg`Play ${link.title}`)}>
|
||||
{(!isPrefetched || !isAnimating) && ( // If we have not loaded or are not animating, show the overlay
|
||||
<View style={[styles.layer, styles.overlayLayer]}>
|
||||
<View style={[styles.overlayContainer, styles.topRadius]}>
|
||||
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
|
||||
<FontAwesomeIcon icon="play" size={42} color="white" />
|
||||
) : (
|
||||
// Activity indicator while gif loads
|
||||
<ActivityIndicator size="large" color="white" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
@@ -150,6 +146,35 @@ export function ExternalGifEmbed({
|
||||
accessibilityHint={link.title}
|
||||
cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios
|
||||
/>
|
||||
|
||||
{(!isPrefetched || !isAnimating) && (
|
||||
<Fill style={[a.align_center, a.justify_center]}>
|
||||
<Fill
|
||||
style={[
|
||||
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
|
||||
{
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{!isAnimating || !isPlayerActive ? ( // Play button when not animating or not active
|
||||
<PlayButtonIcon />
|
||||
) : (
|
||||
// Activity indicator while gif loads
|
||||
<ActivityIndicator size="large" color="white" />
|
||||
)}
|
||||
</Fill>
|
||||
)}
|
||||
<MediaInsetBorder
|
||||
opaque
|
||||
style={[
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
@@ -171,7 +196,6 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
overlayLayer: {
|
||||
zIndex: 2,
|
||||
|
||||
@@ -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 = ({
|
||||
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden]}>
|
||||
<LinkWrapper link={link} onOpen={onOpen} style={style}>
|
||||
{imageUri && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{
|
||||
aspectRatio: 1.91,
|
||||
borderTopRightRadius: 6,
|
||||
borderTopLeftRadius: 6,
|
||||
}}
|
||||
source={{uri: imageUri}}
|
||||
accessibilityIgnoresInvertColors
|
||||
accessibilityLabel={starterPackParsed ? link.title : undefined}
|
||||
accessibilityHint={
|
||||
starterPackParsed ? _(msg`Navigate to starter pack`) : undefined
|
||||
}
|
||||
/>
|
||||
<View>
|
||||
<Image
|
||||
style={{
|
||||
aspectRatio: 1.91,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
}}
|
||||
source={{uri: imageUri}}
|
||||
accessibilityIgnoresInvertColors
|
||||
accessibilityLabel={starterPackParsed ? link.title : undefined}
|
||||
accessibilityHint={
|
||||
starterPackParsed ? _(msg`Navigate to starter pack`) : undefined
|
||||
}
|
||||
/>
|
||||
<MediaInsetBorder
|
||||
opaque
|
||||
style={[
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
) : undefined}
|
||||
{embedPlayerParams?.isGif ? (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
@@ -81,11 +94,18 @@ export const ExternalLinkEmbed = ({
|
||||
) : undefined}
|
||||
<View
|
||||
style={[
|
||||
a.border_b,
|
||||
a.border_l,
|
||||
a.border_r,
|
||||
a.flex_1,
|
||||
a.py_sm,
|
||||
t.atoms.border_contrast_low,
|
||||
{
|
||||
borderBottomRightRadius: 8,
|
||||
borderBottomLeftRadius: 8,
|
||||
paddingHorizontal: isMobile ? 10 : 14,
|
||||
},
|
||||
!imageUri && !embedPlayerParams && [a.border, a.rounded_sm],
|
||||
]}>
|
||||
<Text
|
||||
type="sm"
|
||||
@@ -124,8 +144,6 @@ function LinkWrapper({
|
||||
style?: StyleProp<ViewStyle>
|
||||
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}
|
||||
|
||||
@@ -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)
|
||||
/>
|
||||
|
||||
<MediaInsetBorder
|
||||
opaque
|
||||
style={[
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
@@ -118,6 +130,7 @@ export function ExternalPlayer({
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const insets = useSafeAreaInsets()
|
||||
const windowDims = useWindowDimensions()
|
||||
@@ -211,13 +224,38 @@ export function ExternalPlayer({
|
||||
onAccept={onAcceptConsent}
|
||||
/>
|
||||
|
||||
<Animated.View ref={viewRef} collapsable={false} style={[aspect]}>
|
||||
<Animated.View
|
||||
ref={viewRef}
|
||||
collapsable={false}
|
||||
style={[aspect, a.rounded_sm]}>
|
||||
{link.thumb && (!isPlayerActive || isLoading) && (
|
||||
<Image
|
||||
style={[a.flex_1, styles.topRadius]}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
<>
|
||||
<Image
|
||||
style={[a.flex_1, styles.topRadius]}
|
||||
source={{uri: link.thumb}}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
<Fill
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<MediaInsetBorder
|
||||
opaque
|
||||
style={[
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<PlaceholderOverlay
|
||||
isLoading={isLoading}
|
||||
@@ -236,14 +274,13 @@ export function ExternalPlayer({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
topRadius: {
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
},
|
||||
overlayContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
overlayLayer: {
|
||||
zIndex: 2,
|
||||
@@ -252,6 +289,8 @@ const styles = StyleSheet.create({
|
||||
zIndex: 3,
|
||||
},
|
||||
webview: {
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
gifContainer: {
|
||||
|
||||
@@ -18,7 +18,9 @@ import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
import {EmbedPlayerParams} from 'lib/strings/embed-player'
|
||||
import {useAutoplayDisabled} from 'state/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
@@ -56,8 +58,6 @@ function PlaybackControls({
|
||||
zIndex: 2,
|
||||
backgroundColor: !isLoaded
|
||||
? t.atoms.bg_contrast_25.backgroundColor
|
||||
: !isPlaying
|
||||
? 'rgba(0, 0, 0, 0.3)'
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
@@ -86,6 +86,7 @@ export function GifEmbed({
|
||||
hideAlt?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
|
||||
@@ -138,6 +139,17 @@ export function GifEmbed({
|
||||
accessibilityHint={_(msg`Animated GIF`)}
|
||||
accessibilityLabel={parsedAlt.alt}
|
||||
/>
|
||||
{!playerState.isPlaying && (
|
||||
<Fill
|
||||
style={[
|
||||
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
|
||||
{
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<MediaInsetBorder />
|
||||
{!hideAlt && parsedAlt.isPreferred && <AltText text={parsedAlt.alt} />}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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 (
|
||||
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||
<View
|
||||
style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Blocked</Trans>
|
||||
@@ -84,7 +86,8 @@ export function MaybeQuoteEmbed({
|
||||
)
|
||||
} else if (AppBskyEmbedRecord.isViewNotFound(embed.record)) {
|
||||
return (
|
||||
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||
<View
|
||||
style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Deleted</Trans>
|
||||
@@ -96,7 +99,8 @@ export function MaybeQuoteEmbed({
|
||||
? embed.record.uri.includes(currentAccount.did)
|
||||
: false
|
||||
return (
|
||||
<View style={[styles.errorContainer, pal.borderDark]}>
|
||||
<View
|
||||
style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{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 (
|
||||
<ContentHider
|
||||
modui={moderation?.ui('contentList')}
|
||||
style={[styles.container, pal.borderDark, style]}
|
||||
style={[styles.container, a.border, t.atoms.border_contrast_low, style]}
|
||||
childContainerStyle={[a.pt_sm]}>
|
||||
<Link
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import React, {useCallback, useEffect, useId, useState} from 'react'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {PlayerError, VideoPlayerStatus} from 'expo-video'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {useAutoplayDisabled} from 'state/preferences'
|
||||
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
import {useActiveVideoNative} from './ActiveVideoNativeContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
interface Props {
|
||||
@@ -59,113 +55,36 @@ export function VideoEmbed({embed}: Props) {
|
||||
|
||||
function InnerWrapper({embed}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {activeSource, activeViewId, setActiveSource, player} =
|
||||
useActiveVideoNative()
|
||||
const viewId = useId()
|
||||
const ref = React.useRef<{togglePlayback: () => 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<Error | PlayerError | null>(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 (
|
||||
<VisibilityView enabled={true} onChangeStatus={onVisibilityStatusChange}>
|
||||
{isActive ? (
|
||||
<VideoEmbedInnerNative
|
||||
embed={embed}
|
||||
timeRemaining={timeRemaining}
|
||||
isMuted={isMuted}
|
||||
isFullscreen={isFullscreen}
|
||||
setIsFullscreen={setIsFullscreen}
|
||||
/>
|
||||
) : null}
|
||||
<>
|
||||
<VideoEmbedInnerNative
|
||||
embed={embed}
|
||||
setStatus={setStatus}
|
||||
setIsLoading={setIsLoading}
|
||||
setIsActive={setIsActive}
|
||||
ref={ref}
|
||||
/>
|
||||
<ImageBackground
|
||||
source={{uri: embed.thumbnail}}
|
||||
accessibilityIgnoresInvertColors
|
||||
@@ -185,17 +104,18 @@ function InnerWrapper({embed}: Props) {
|
||||
>
|
||||
<Button
|
||||
style={[a.flex_1, a.align_center, a.justify_center]}
|
||||
onPress={() => startPlaying(true)}
|
||||
onPress={() => {
|
||||
ref.current?.togglePlayback()
|
||||
}}
|
||||
label={_(msg`Play video`)}
|
||||
color="secondary">
|
||||
{isLoading ? (
|
||||
{showSpinner ? (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.p_xs,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{backgroundColor: 'rgba(0,0,0,0.5)'},
|
||||
]}>
|
||||
<Loader size="2xl" style={{color: 'white'}} />
|
||||
</View>
|
||||
@@ -204,7 +124,7 @@ function InnerWrapper({embed}: Props) {
|
||||
)}
|
||||
</Button>
|
||||
</ImageBackground>
|
||||
</VisibilityView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (isNaN(time)) {
|
||||
@@ -22,18 +29,20 @@ export function TimeIndicator({time}: {time: number}) {
|
||||
<Animated.View
|
||||
entering={native(FadeInDown.duration(300))}
|
||||
exiting={native(FadeOutDown.duration(500))}
|
||||
pointerEvents="none"
|
||||
style={[
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
left: 6,
|
||||
bottom: 6,
|
||||
minHeight: 21,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
a.absolute,
|
||||
a.justify_center,
|
||||
style,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
|
||||
@@ -1,134 +1,134 @@
|
||||
import React, {useCallback, useRef} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import React, {useRef} from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import Animated, {FadeInDown} from 'react-native-reanimated'
|
||||
import {VideoPlayer, VideoView} from 'expo-video'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useVideoVolumeState} from 'view/com/util/post-embeds/VideoVolumeContext'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
|
||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {
|
||||
AudioCategory,
|
||||
PlatformInfo,
|
||||
} from '../../../../../../modules/expo-bluesky-swiss-army'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export function VideoEmbedInnerNative({
|
||||
embed,
|
||||
isFullscreen,
|
||||
setIsFullscreen,
|
||||
isMuted,
|
||||
timeRemaining,
|
||||
}: {
|
||||
embed: AppBskyEmbedVideo.View
|
||||
isFullscreen: boolean
|
||||
setIsFullscreen: (isFullscreen: boolean) => void
|
||||
timeRemaining: number
|
||||
isMuted: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {player} = useActiveVideoNative()
|
||||
const ref = useRef<VideoView>(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<BlueskyVideoView>(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<string>()
|
||||
|
||||
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 (
|
||||
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
|
||||
<VideoView
|
||||
ref={ref}
|
||||
player={player}
|
||||
style={[a.flex_1, a.rounded_sm]}
|
||||
contentFit="cover"
|
||||
nativeControls={isFullscreen}
|
||||
accessibilityIgnoresInvertColors
|
||||
onFullscreenEnter={() => {
|
||||
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 (
|
||||
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
beginMuted={autoplayDisabled ? false : muted}
|
||||
style={[a.rounded_sm]}
|
||||
onActiveChange={e => {
|
||||
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=""
|
||||
/>
|
||||
<VideoControls
|
||||
player={player}
|
||||
enterFullscreen={enterFullscreen}
|
||||
isMuted={isMuted}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
<VideoControls
|
||||
enterFullscreen={() => {
|
||||
videoRef.current?.enterFullscreen()
|
||||
}}
|
||||
toggleMuted={() => {
|
||||
videoRef.current?.toggleMuted()
|
||||
}}
|
||||
togglePlayback={() => {
|
||||
videoRef.current?.togglePlayback()
|
||||
}}
|
||||
isPlaying={isPlaying}
|
||||
timeRemaining={timeRemaining}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
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 (
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
{showTime && <TimeIndicator time={timeRemaining} />}
|
||||
<Pressable
|
||||
onPress={onPressFullscreen}
|
||||
onPress={enterFullscreen}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={_(msg`Video`)}
|
||||
accessibilityHint={_(msg`Tap to enter full screen`)}
|
||||
accessibilityRole="button"
|
||||
/>
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300)}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
a.justify_center,
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
bottom: 6,
|
||||
right: 6,
|
||||
minHeight: 21,
|
||||
minWidth: 21,
|
||||
},
|
||||
]}>
|
||||
<Pressable
|
||||
onPress={toggleMuted}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
|
||||
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||
accessibilityRole="button"
|
||||
hitSlop={HITSLOP_30}>
|
||||
{isMuted ? (
|
||||
<MuteIcon width={13} fill={t.palette.white} />
|
||||
) : (
|
||||
<UnmuteIcon width={13} fill={t.palette.white} />
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
<ControlButton
|
||||
onPress={togglePlayback}
|
||||
label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
|
||||
accessibilityHint={_(msg`Tap to play or pause`)}
|
||||
style={{left: 6}}>
|
||||
{isPlaying ? (
|
||||
<PauseIcon width={13} fill={t.palette.white} />
|
||||
) : (
|
||||
<PlayIcon width={13} fill={t.palette.white} />
|
||||
)}
|
||||
</ControlButton>
|
||||
{showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
|
||||
|
||||
<ControlButton
|
||||
onPress={toggleMuted}
|
||||
label={muted ? _(msg`Unmute`) : _(msg`Mute`)}
|
||||
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||
style={{right: 6}}>
|
||||
{muted ? (
|
||||
<MuteIcon width={13} fill={t.palette.white} />
|
||||
) : (
|
||||
<UnmuteIcon width={13} fill={t.palette.white} />
|
||||
)}
|
||||
</ControlButton>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ControlButton({
|
||||
onPress,
|
||||
children,
|
||||
label,
|
||||
accessibilityHint,
|
||||
style,
|
||||
}: {
|
||||
onPress: () => void
|
||||
children: React.ReactNode
|
||||
label: string
|
||||
accessibilityHint: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300)}
|
||||
style={[
|
||||
a.absolute,
|
||||
a.rounded_full,
|
||||
a.justify_center,
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
bottom: 6,
|
||||
minHeight: 21,
|
||||
minWidth: 21,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={label}
|
||||
accessibilityHint={accessibilityHint}
|
||||
accessibilityRole="button"
|
||||
hitSlop={HITSLOP_30}>
|
||||
{children}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</div>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -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 <Context.Provider value={value}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export function useVideoVolumeState() {
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useVideoVolumeState must be used within a VideoVolumeProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -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 (
|
||||
<Svg
|
||||
fill="none"
|
||||
viewBox="0 0 61 54"
|
||||
{...rest}
|
||||
width={size}
|
||||
height={Number(size) * ratio}>
|
||||
<Path
|
||||
fill={fill || pal.text.color}
|
||||
d="M13.223 3.602C20.215 8.832 27.738 19.439 30.5 25.13c2.762-5.691 10.284-16.297 17.278-21.528C52.824-.172 61-3.093 61 6.2c0 1.856-1.068 15.59-1.694 17.82-2.178 7.752-10.112 9.73-17.17 8.532 12.337 2.092 15.475 9.021 8.697 15.95-12.872 13.159-18.5-3.302-19.943-7.52-.264-.773-.388-1.135-.39-.827-.002-.308-.126.054-.39.827-1.442 4.218-7.071 20.679-19.943 7.52-6.778-6.929-3.64-13.858 8.697-15.95-7.058 1.197-14.992-.78-17.17-8.532C1.068 21.79 0 8.056 0 6.2 0-3.093 8.176-.172 13.223 3.602Z"
|
||||
/>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
@@ -63,6 +63,7 @@ export function Composer({}: {winHeight: number}) {
|
||||
mention={state.mention}
|
||||
openEmojiPicker={onOpenPicker}
|
||||
text={state.text}
|
||||
imageUris={state.imageUris}
|
||||
/>
|
||||
</View>
|
||||
<EmojiPicker state={pickerState} close={onClosePicker} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user