Merge remote-tracking branch 'origin/main' into font-scaling
* origin/main: (24 commits) Fix border radius on avatars (#5392) Make like animation on web same speed as mobile (#5391) Pre-fill alt text with 10-million card post (#5389) Tweak `pt-BR` string (#5372) Bump 1.91.2 (#5387) [Video] Additional android fixes (#5373) Milly tweaks (#5365) [Video] Fix safari showing spinner (#5364) [Video] Volume controls on web (#5363) Updates to Irish translation, back to 100% (#5345) Update catalan messages.po (#5223) Update Japanese translation (#5217) Update Korean localization (#5254) Update Chinese localization (#5291) Update French localization (#5227) Update Portuguese localization (#5256) [Video] Fix scrubber tap target (#5360) loosen checks on vtt file (#5359) Remove overflow hidden from external link outer el (#5356) Disallow some font scaling ...
This commit is contained in:
@@ -75,6 +75,7 @@ export function LikedByList({uri}: {uri: string}) {
|
||||
isLoading={isUriLoading || isLikedByLoading}
|
||||
isError={isError}
|
||||
emptyType="results"
|
||||
emptyTitle={_(msg`No likes yet`)}
|
||||
emptyMessage={_(
|
||||
msg`Nobody has liked this yet. Maybe you should be the first!`,
|
||||
)}
|
||||
|
||||
@@ -59,7 +59,9 @@ export function Outer({
|
||||
export function TitleText({children}: React.PropsWithChildren<{}>) {
|
||||
const {titleId} = React.useContext(Context)
|
||||
return (
|
||||
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
|
||||
<Text
|
||||
nativeID={titleId}
|
||||
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Svg, {Circle, Path} from 'react-native-svg'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
|
||||
import {atoms as a, ViewStyleProp} from '#/alf'
|
||||
import {Button, ButtonProps} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {TenMillion} from './'
|
||||
|
||||
export function Trigger({children}: {children: ButtonProps['children']}) {
|
||||
const {_} = useLingui()
|
||||
const {mutate: upsertNux} = useUpsertNuxMutation()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const [fallback, setFallback] = React.useState(false)
|
||||
const control = Prompt.usePromptControl()
|
||||
|
||||
const handleOnPress = () => {
|
||||
if (!fallback) {
|
||||
setShow(true)
|
||||
upsertNux({
|
||||
id: Nux.TenMillionDialog,
|
||||
completed: true,
|
||||
data: undefined,
|
||||
})
|
||||
} else {
|
||||
control.open()
|
||||
}
|
||||
}
|
||||
|
||||
const onHandleFallback = () => {
|
||||
setFallback(true)
|
||||
control.open()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
label={_(msg`Bluesky is celebrating 10 million users!`)}
|
||||
onPress={handleOnPress}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
{show && !fallback && (
|
||||
<TenMillion
|
||||
showTimeout={0}
|
||||
onClose={() => setShow(false)}
|
||||
onFallback={onHandleFallback}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Prompt.Outer control={control}>
|
||||
<View style={{maxWidth: 300}}>
|
||||
<Prompt.TitleText>
|
||||
<Trans>Bluesky is celebrating 10 million users!</Trans>
|
||||
</Prompt.TitleText>
|
||||
</View>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
Together, we're rebuilding the social internet. We're glad you're
|
||||
here.
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Prompt.DescriptionText>
|
||||
<Trans>
|
||||
To learn more,{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`View our post`)}
|
||||
to="/profile/bsky.app/post/3l47prg3wgy23"
|
||||
onPress={() => {
|
||||
control.close()
|
||||
}}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>check out our post.</Trans>
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Prompt.DescriptionText>
|
||||
<Dialog.Close />
|
||||
</Prompt.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon({width, style}: {width: number} & ViewStyleProp) {
|
||||
return (
|
||||
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
|
||||
<Path
|
||||
fill="#dd2e44"
|
||||
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
|
||||
/>
|
||||
<Path
|
||||
fill="#ea596e"
|
||||
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
|
||||
/>
|
||||
<Path
|
||||
fill="#a0041e"
|
||||
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
|
||||
/>
|
||||
<Path
|
||||
fill="#aa8dd8"
|
||||
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
|
||||
/>
|
||||
<Path
|
||||
fill="#77b255"
|
||||
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
|
||||
/>
|
||||
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
|
||||
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
|
||||
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
|
||||
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
|
||||
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,15 @@ function Frame({children}: {children: React.ReactNode}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TenMillion() {
|
||||
export function TenMillion({
|
||||
showTimeout,
|
||||
onClose,
|
||||
onFallback,
|
||||
}: {
|
||||
showTimeout?: number
|
||||
onClose?: () => void
|
||||
onFallback?: () => void
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const [userNumber, setUserNumber] = React.useState<number>(0)
|
||||
@@ -115,12 +123,16 @@ export function TenMillion() {
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (data.number) {
|
||||
if (data.number && data.number <= 10_000_000) {
|
||||
setUserNumber(data.number)
|
||||
} else {
|
||||
// should be rare
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
} else {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +140,7 @@ export function TenMillion() {
|
||||
fetching.current = true
|
||||
networkRetry(3, fetchUserNumber).catch(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
onFallback?.()
|
||||
})
|
||||
}
|
||||
}, [
|
||||
@@ -136,12 +149,27 @@ export function TenMillion() {
|
||||
setUserNumber,
|
||||
nuxDialogs.dismissActiveNux,
|
||||
nuxDialogs,
|
||||
onFallback,
|
||||
])
|
||||
|
||||
return userNumber ? <TenMillionInner userNumber={userNumber} /> : null
|
||||
return userNumber ? (
|
||||
<TenMillionInner
|
||||
userNumber={userNumber}
|
||||
showTimeout={showTimeout ?? 3e3}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
export function TenMillionInner({
|
||||
userNumber,
|
||||
showTimeout,
|
||||
onClose: onCloseOuter,
|
||||
}: {
|
||||
userNumber: number
|
||||
showTimeout: number
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const lightTheme = useTheme('light')
|
||||
const {_, i18n} = useLingui()
|
||||
@@ -169,6 +197,27 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
const isLoadingData = isProfileLoading || !moderation || !profile
|
||||
const isLoadingImage = !uri
|
||||
|
||||
const displayName = React.useMemo(() => {
|
||||
if (!profile || !moderation) return ''
|
||||
return sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
}, [profile, moderation])
|
||||
const handle = React.useMemo(() => {
|
||||
if (!profile) return ''
|
||||
return sanitizeHandle(profile.handle, '@')
|
||||
}, [profile])
|
||||
const joinedDate = React.useMemo(() => {
|
||||
if (!profile || !profile.createdAt) return ''
|
||||
const date = i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
return date
|
||||
}, [i18n, profile])
|
||||
|
||||
const error: string = React.useMemo(() => {
|
||||
if (profileError) {
|
||||
return _(
|
||||
@@ -184,14 +233,15 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
React.useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
control.open()
|
||||
}, 3e3)
|
||||
}, showTimeout)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [control])
|
||||
}, [control, showTimeout])
|
||||
const onClose = React.useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
onCloseOuter?.()
|
||||
}, [nuxDialogs, onCloseOuter])
|
||||
|
||||
/*
|
||||
* Actions
|
||||
@@ -206,19 +256,34 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
|
||||
userNumber,
|
||||
)}!`,
|
||||
), // TODO
|
||||
),
|
||||
imageUris: [
|
||||
{
|
||||
uri,
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
altText: _(
|
||||
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
|
||||
userNumber,
|
||||
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
}, 1e3)
|
||||
})
|
||||
}
|
||||
}, [_, i18n, control, openComposer, uri, userNumber])
|
||||
}, [
|
||||
_,
|
||||
i18n,
|
||||
control,
|
||||
openComposer,
|
||||
uri,
|
||||
userNumber,
|
||||
displayName,
|
||||
handle,
|
||||
joinedDate,
|
||||
])
|
||||
const onNativeShare = React.useCallback(() => {
|
||||
if (uri) {
|
||||
control.close(() => {
|
||||
@@ -358,6 +423,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
@@ -372,6 +438,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
</Text>
|
||||
<View style={[a.flex_row, a.align_start]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.absolute,
|
||||
{
|
||||
@@ -391,6 +458,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
#
|
||||
</Text>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.relative,
|
||||
a.text_center,
|
||||
@@ -450,6 +518,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
*/}
|
||||
<View style={[a.gap_2xs, a.flex_1]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
@@ -457,15 +526,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.leading_tight,
|
||||
{maxWidth: '60%'},
|
||||
]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName ||
|
||||
sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
{displayName}
|
||||
</Text>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
a.flex_1,
|
||||
@@ -474,11 +540,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.leading_snug,
|
||||
lightTheme.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{sanitizeHandle(profile.handle, '@')}
|
||||
{handle}
|
||||
</Text>
|
||||
|
||||
{profile.createdAt && (
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="head"
|
||||
style={[
|
||||
@@ -489,14 +556,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.text_right,
|
||||
lightTheme.atoms.text_contrast_low,
|
||||
]}>
|
||||
<Trans>
|
||||
Joined{' '}
|
||||
{i18n.date(profile.createdAt, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Trans>
|
||||
<Trans>Joined on {joinedDate}</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
@@ -582,6 +642,7 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
|
||||
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
|
||||
<Text
|
||||
allowFontScaling={false}
|
||||
style={[
|
||||
a.text_5xl,
|
||||
a.leading_tight,
|
||||
@@ -610,9 +671,12 @@ export function TenMillionInner({userNumber}: {userNumber: number}) {
|
||||
a.gap_md,
|
||||
a.pt_xl,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
{gtMobile && (
|
||||
<Text
|
||||
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Brag a little!</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={isLoadingImage}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useUpsertNuxMutation,
|
||||
} from '#/state/queries/nuxs'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
|
||||
import {IS_DEV} from '#/env'
|
||||
@@ -18,31 +19,12 @@ type Context = {
|
||||
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
|
||||
enabled?: (props: {gate: ReturnType<typeof useGate>}) => boolean
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.TenMillionDialog,
|
||||
enabled({gate}) {
|
||||
return gate('ten_million_dialog')
|
||||
},
|
||||
unsafe_disableSnooze: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -57,7 +39,8 @@ export function useNuxDialogContext() {
|
||||
|
||||
export function NuxDialogs() {
|
||||
const {hasSession} = useSession()
|
||||
return hasSession ? <Inner /> : null
|
||||
const onboardingState = useOnboardingState()
|
||||
return hasSession && !onboardingState.isActive ? <Inner /> : null
|
||||
}
|
||||
|
||||
function Inner() {
|
||||
@@ -90,30 +73,23 @@ function Inner() {
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (__isSnoozedFallback) return
|
||||
if (snoozed) return
|
||||
if (!nuxs) return
|
||||
|
||||
for (const {id, enabled, unsafe_disableSnooze} of queuedNuxs) {
|
||||
for (const {id, enabled} 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
|
||||
if (enabled && !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 snooze for a day
|
||||
snoozeNuxDialog()
|
||||
|
||||
// immediately update remote data (affects next reload)
|
||||
upsertNux({
|
||||
@@ -124,12 +100,6 @@ function Inner() {
|
||||
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
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '#/components/icons/Heart2'
|
||||
|
||||
const animationConfig = {
|
||||
duration: 400,
|
||||
duration: 600,
|
||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
fill: 'forwards' as FillMode,
|
||||
}
|
||||
|
||||
@@ -97,10 +97,6 @@ export 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)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext'
|
||||
| 'typography_neue'
|
||||
| 'suggested_feeds_interstitial'
|
||||
| 'ten_million_dialog'
|
||||
'debug_show_feedcontext' | 'typography_neue' | 'suggested_feeds_interstitial'
|
||||
|
||||
@@ -31,11 +31,11 @@ msgstr "{0, plural, one {{formattedCount} other} other {{formattedCount} others}
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:156
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# dia} other {# dies}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:146
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# hora} other {# hores}}"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:168
|
||||
#~ msgid "{0, plural, one {# invite code available} other {# invite codes available}}"
|
||||
@@ -59,11 +59,11 @@ msgstr "{0, plural, one {# etiqueta s'ha aplicat a aquest contingut} other {# et
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:136
|
||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# minut} other {# minuts}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:167
|
||||
msgid "{0, plural, one {# month} other {# months}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# mes} other {# mesos}}"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:71
|
||||
msgid "{0, plural, one {# repost} other {# reposts}}"
|
||||
@@ -71,11 +71,11 @@ msgstr "{0, plural, one {# republicació} other {# republicacions}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:126
|
||||
msgid "{0, plural, one {# second} other {# seconds}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# segon} other {# segons}}"
|
||||
|
||||
#: src/components/KnownFollowers.tsx:179
|
||||
#~ msgid "{0, plural, one {and # other} other {and # others}}"
|
||||
#~ msgstr ""
|
||||
#~ msgstr "{0, plural, one {i # altre} other {i # altres}}"
|
||||
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:398
|
||||
#: src/screens/Profile/Header/Metrics.tsx:23
|
||||
@@ -144,7 +144,7 @@ msgstr "{0} s'han unit aquesta setmana"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:637
|
||||
msgid "{0} of {1}"
|
||||
msgstr ""
|
||||
msgstr "{0} de {1}"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:467
|
||||
msgid "{0} people have used this starter pack!"
|
||||
@@ -169,27 +169,27 @@ msgstr "Starter pack de {0}"
|
||||
#. How many days have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:158
|
||||
msgid "{0}d"
|
||||
msgstr ""
|
||||
msgstr "{0}d"
|
||||
|
||||
#. How many hours have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:148
|
||||
msgid "{0}h"
|
||||
msgstr ""
|
||||
msgstr "{0}h"
|
||||
|
||||
#. How many minutes have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:138
|
||||
msgid "{0}m"
|
||||
msgstr ""
|
||||
msgstr "{0}m"
|
||||
|
||||
#. How many months have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:169
|
||||
msgid "{0}mo"
|
||||
msgstr ""
|
||||
msgstr "{0}me"
|
||||
|
||||
#. How many seconds have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:128
|
||||
msgid "{0}s"
|
||||
msgstr ""
|
||||
msgstr "{0}s"
|
||||
|
||||
#: src/components/LabelingServiceCard/index.tsx:71
|
||||
msgid "{count, plural, one {Liked by # user} other {Liked by # users}}"
|
||||
@@ -504,7 +504,7 @@ msgstr "Afegeix text alternatiu"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:109
|
||||
msgid "Add alt text (optional)"
|
||||
msgstr ""
|
||||
msgstr "Afegeix text alternatiu (opcional)"
|
||||
|
||||
#: src/view/screens/AppPasswords.tsx:105
|
||||
#: src/view/screens/AppPasswords.tsx:147
|
||||
@@ -700,7 +700,7 @@ msgstr "Hi ha hagut un error"
|
||||
|
||||
#: src/state/queries/video/video.ts:232
|
||||
msgid "An error occurred while compressing the video."
|
||||
msgstr ""
|
||||
msgstr "Hi ha hagut un error mentre es comprimia el vídeo."
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:315
|
||||
msgid "An error occurred while generating your starter pack. Want to try again?"
|
||||
@@ -712,7 +712,7 @@ msgstr "Hi ha hagut un error mentre es carregava el vídeo. Prova-ho més tard."
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:174
|
||||
msgid "An error occurred while loading the video. Please try again."
|
||||
msgstr ""
|
||||
msgstr "Hi ha hagut un error mentre es carregava el vídeo. Prova-ho més tard."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
|
||||
msgid "An error occurred while saving the image!"
|
||||
@@ -729,7 +729,7 @@ msgstr "S'ha produït un error en desar el codi QR!"
|
||||
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:75
|
||||
msgid "An error occurred while selecting the video"
|
||||
msgstr ""
|
||||
msgstr "Hi ha hagut un error mentre es seleccionava el vídeo"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:134
|
||||
#~ msgid "An error occurred while trying to delete the message. Please try again."
|
||||
@@ -1286,15 +1286,15 @@ msgstr "Cancel·la obrir la web enllaçada"
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:155
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:191
|
||||
msgid "Cannot interact with a blocked user"
|
||||
msgstr ""
|
||||
msgstr "No pots interactuar amb un usuari bloquejat"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:135
|
||||
msgid "Captions (.vtt)"
|
||||
msgstr ""
|
||||
msgstr "Subtítols (.vtt)"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:56
|
||||
msgid "Captions & alt text"
|
||||
msgstr ""
|
||||
msgstr "Subtítols i text alternatiu"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
|
||||
msgid "Celebrating {0} users"
|
||||
@@ -1905,7 +1905,7 @@ msgstr "No s'ha pogut silenciar el xat"
|
||||
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:56
|
||||
msgid "Could not process your video"
|
||||
msgstr ""
|
||||
msgstr "No s'ha pogut processar el teu vídeo"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:68
|
||||
#~ msgid "Could not unmute chat"
|
||||
@@ -2213,7 +2213,7 @@ msgstr "Els missatges directes són aquí!"
|
||||
|
||||
#: src/view/screens/AccessibilitySettings.tsx:111
|
||||
msgid "Disable autoplay for videos and GIFs"
|
||||
msgstr ""
|
||||
msgstr "Desactiva la reproducció automàtica per GIFs i vídeos"
|
||||
|
||||
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:90
|
||||
msgid "Disable Email 2FA"
|
||||
@@ -2558,7 +2558,7 @@ msgstr "Adreça de correu"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "S'ha tornat a enviar el correu"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2575,7 +2575,7 @@ msgstr "Correu verificat"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "Correu verificat"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2662,7 +2662,7 @@ msgstr "Fi del canal"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
|
||||
msgid "Ensure you have selected a language for each subtitle file."
|
||||
msgstr ""
|
||||
msgstr "Assegura't que has seleccionat un idioma per a cada fitxer de subtítols."
|
||||
|
||||
#: src/view/com/modals/AddAppPasswords.tsx:161
|
||||
msgid "Enter a name for this App Password"
|
||||
@@ -2776,7 +2776,7 @@ msgstr "Exclou els usuaris que segueixes"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:397
|
||||
msgid "Exit fullscreen"
|
||||
msgstr ""
|
||||
msgstr "Surt de la pantalla completa"
|
||||
|
||||
#: src/view/com/modals/DeleteAccount.tsx:293
|
||||
msgid "Exits account deletion process"
|
||||
@@ -2961,7 +2961,7 @@ msgstr "No s'ha pogut actualitzar la configuració"
|
||||
#: src/state/queries/video/video-upload.web.ts:68
|
||||
#: src/state/queries/video/video-upload.web.ts:78
|
||||
msgid "Failed to upload video"
|
||||
msgstr ""
|
||||
msgstr "No s'ha pogut pujar el vídeo"
|
||||
|
||||
#: src/Navigation.tsx:226
|
||||
msgid "Feed"
|
||||
@@ -3314,7 +3314,7 @@ msgstr "De <0/>"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:398
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
msgstr "Pantalla completa"
|
||||
|
||||
#: src/view/com/composer/photos/SelectPhotoBtn.tsx:39
|
||||
msgid "Gallery"
|
||||
@@ -3579,7 +3579,7 @@ msgstr "No podem carregar el servei de moderació."
|
||||
|
||||
#: src/state/queries/video/video.ts:165
|
||||
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
|
||||
msgstr ""
|
||||
msgstr "Espera! A poc a poc estem donant accés al vídeo i encara estàs a la cua. Torna més tard!"
|
||||
|
||||
#: src/Navigation.tsx:550
|
||||
#: src/Navigation.tsx:570
|
||||
@@ -3692,7 +3692,7 @@ msgstr "Suplantació d'identitat o afirmacions falses sobre identitat o afiliaci
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:68
|
||||
msgid "Impersonation, misinformation, or false claims"
|
||||
msgstr ""
|
||||
msgstr "Suplantació d'identitat, desinformació o afirmacions falses"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:91
|
||||
msgid "Inappropriate messages or explicit links"
|
||||
@@ -3786,7 +3786,7 @@ msgstr "Nom d'usuari o contrasenya incorrectes"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "Codi de verificació invàlid"
|
||||
|
||||
#: src/view/screens/Settings.tsx:411
|
||||
#~ msgid "Invite"
|
||||
@@ -3838,11 +3838,11 @@ msgstr "Ara només ets tu! Afegeix més persones al teu starter pack cercant a d
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1126
|
||||
msgid "Job ID: {0}"
|
||||
msgstr ""
|
||||
msgstr "Identificador de la tasca: {0}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:164
|
||||
msgid "Jobs"
|
||||
msgstr "Feines"
|
||||
msgstr "Tasques"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:206
|
||||
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:212
|
||||
@@ -4322,7 +4322,7 @@ msgstr "Compte enganyós"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:67
|
||||
msgid "Misleading Post"
|
||||
msgstr ""
|
||||
msgstr "Publicació enganyosa"
|
||||
|
||||
#: src/screens/Settings/AppearanceSettings.tsx:78
|
||||
msgid "Mode"
|
||||
@@ -4983,7 +4983,7 @@ msgstr "Respostes més antigues primer"
|
||||
|
||||
#: src/components/StarterPack/QrCode.tsx:70
|
||||
msgid "on<0><1/><2><3/></2></0>"
|
||||
msgstr ""
|
||||
msgstr "en<0><1/><2><3/></2></0>"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:226
|
||||
msgid "Onboarding reset"
|
||||
@@ -5015,7 +5015,7 @@ msgstr "Només pot tenir lletres, nombres i guionets"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:39
|
||||
msgid "Only WebVTT (.vtt) files are supported"
|
||||
msgstr ""
|
||||
msgstr "Només s'admeten arxius WebVTT (.vtt)"
|
||||
|
||||
#: src/components/Lists.tsx:88
|
||||
msgid "Oops, something went wrong!"
|
||||
@@ -5902,7 +5902,7 @@ msgstr "Elimina el compte"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:28
|
||||
msgid "Remove attachment"
|
||||
msgstr ""
|
||||
msgstr "Elimina l'adjunt"
|
||||
|
||||
#: src/view/com/util/UserAvatar.tsx:393
|
||||
msgid "Remove Avatar"
|
||||
@@ -5978,7 +5978,7 @@ msgstr "Elimina la republicació"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:264
|
||||
msgid "Remove subtitle file"
|
||||
msgstr ""
|
||||
msgstr "Eliminar arxiu de subtítols"
|
||||
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:175
|
||||
#~ msgid "Remove this feed from my feeds?"
|
||||
@@ -6030,7 +6030,7 @@ msgstr "Elimina la publicació amb la citació"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
|
||||
msgid "Removes the attachment"
|
||||
msgstr ""
|
||||
msgstr "Elimina l'adjunt"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
|
||||
#~ msgid "Removes the image preview"
|
||||
@@ -6297,11 +6297,11 @@ msgstr "Torna a enviar el correu"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "Torna a enviar el correu"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "Torna a enviar el correu de verificació"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -6601,7 +6601,7 @@ msgstr "Consulta aquesta guia"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:631
|
||||
msgid "Seek slider"
|
||||
msgstr ""
|
||||
msgstr "Cerca el control lliscant"
|
||||
|
||||
#: src/view/com/util/Selector.tsx:106
|
||||
msgid "Select {item}"
|
||||
@@ -6645,7 +6645,7 @@ msgstr "Tria per quant temps s'ha de silenciar aquesta paraula."
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:249
|
||||
msgid "Select language..."
|
||||
msgstr ""
|
||||
msgstr "Selecciona l'idioma..."
|
||||
|
||||
#: src/view/screens/LanguageSettings.tsx:303
|
||||
msgid "Select languages"
|
||||
@@ -6670,7 +6670,7 @@ msgstr "Selecciona l'opció {i} de {numItems}"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:65
|
||||
msgid "Select subtitle file (.vtt)"
|
||||
msgstr ""
|
||||
msgstr "Tria el arxiu de subtítols (.vtt)"
|
||||
|
||||
#: src/screens/Onboarding/StepProfile/AvatarCreatorItems.tsx:83
|
||||
msgid "Select the {emojiName} emoji as your avatar"
|
||||
@@ -7566,7 +7566,7 @@ msgstr "Toca per canviar el so"
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:219
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:239
|
||||
msgid "Tap to view full image"
|
||||
msgstr ""
|
||||
msgstr "Toca per a veure la imatge completa"
|
||||
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:70
|
||||
#~ msgid "Tap to view fully"
|
||||
@@ -7640,7 +7640,7 @@ msgstr ""
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
|
||||
msgid "Thanks, you have successfully verified your email address."
|
||||
msgstr ""
|
||||
msgstr "Gràcies, has verificat correctament el teu correu."
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -7749,7 +7749,7 @@ msgstr "Les condicions del servei han estat traslladades a"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "El codi de verificació que has proporcionat no és vàlid. Assegura't que has utilitzat l'enllaç de verificació correcte o sol·licita'n un de nou."
|
||||
|
||||
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:141
|
||||
#~ msgid "There are many feeds to try:"
|
||||
@@ -8155,7 +8155,7 @@ msgstr "Per informar d'una conversa, informa d'un dels seus missatges a través
|
||||
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:120
|
||||
msgid "To upload videos to Bluesky, you must first verify your email."
|
||||
msgstr ""
|
||||
msgstr "Primer has de verificar el teu correu per poder pujar vídeos a Bluesky."
|
||||
|
||||
#: src/components/ReportDialog/SelectLabelerView.tsx:33
|
||||
msgid "To whom would you like to send this report?"
|
||||
@@ -8397,7 +8397,7 @@ msgstr "T'has dona't de baixa de la llista"
|
||||
|
||||
#: src/state/queries/video/video.ts:245
|
||||
msgid "Unsupported video type: {mimeType}"
|
||||
msgstr ""
|
||||
msgstr "Tipus de vídeo no compatible: {mimeType}"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:85
|
||||
#~ msgid "Unwanted sexual content"
|
||||
@@ -8600,7 +8600,7 @@ msgstr "Valor:"
|
||||
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:118
|
||||
msgid "Verified email required"
|
||||
msgstr ""
|
||||
msgstr "Es requereix un correu verificat"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:510
|
||||
#~ msgid "Verify {0}"
|
||||
@@ -8616,7 +8616,7 @@ msgstr "Verifica el correu"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "Diàleg de verificació del correu"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -8633,7 +8633,7 @@ msgstr "Verifica el correu nou"
|
||||
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:122
|
||||
msgid "Verify now"
|
||||
msgstr ""
|
||||
msgstr "Verifica-ho ara"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:505
|
||||
msgid "Verify Text File"
|
||||
@@ -8658,7 +8658,7 @@ msgstr "Vídeo"
|
||||
|
||||
#: src/state/queries/video/video.ts:138
|
||||
msgid "Video failed to process"
|
||||
msgstr ""
|
||||
msgstr "El vídeo no s'ha pogut processar"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:39
|
||||
#: src/screens/Onboarding/state.ts:88
|
||||
@@ -8667,15 +8667,15 @@ msgstr "Videojocs"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbed.web.tsx:167
|
||||
msgid "Video not found."
|
||||
msgstr ""
|
||||
msgstr "No s'ha trobat el vídeo."
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:101
|
||||
msgid "Video settings"
|
||||
msgstr ""
|
||||
msgstr "Configuració de vídeo"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
|
||||
msgid "Video: {0}"
|
||||
msgstr ""
|
||||
msgstr "Vídeo: {0}"
|
||||
|
||||
#: src/view/com/composer/videos/state.ts:27
|
||||
#~ msgid "Videos cannot be larger than 50MB"
|
||||
@@ -8801,7 +8801,7 @@ msgstr "Calculem {estimatedTime} fins que el teu compte estigui llest."
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "Hem enviat un altre correu de verificació a <0>{0}</0>."
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -8821,7 +8821,7 @@ msgstr "Ja no hi ha més publicacions dels usuaris que segueixes. Aquí n'hi ha
|
||||
|
||||
#: src/state/queries/video/video.ts:170
|
||||
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
|
||||
msgstr ""
|
||||
msgstr "No hem pogut determinar si tens permís per pujar vídeos. Torna-ho a provar."
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:52
|
||||
msgid "We were unable to load your birth date preferences. Please try again."
|
||||
@@ -9056,7 +9056,7 @@ msgstr "Estàs a la cua."
|
||||
|
||||
#: src/state/queries/video/video.ts:161
|
||||
msgid "You are not allowed to upload videos."
|
||||
msgstr ""
|
||||
msgstr "No t'és permés pujar vídeos."
|
||||
|
||||
#: src/view/com/profile/ProfileFollows.tsx:95
|
||||
msgid "You are not following anyone."
|
||||
@@ -9205,7 +9205,7 @@ msgstr "Has arribat al final"
|
||||
|
||||
#: src/state/queries/video/video-upload.shared.ts:67
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
msgstr "Has arribat temporalment al límit de pujades de vídeos. Torna-ho a provar més tard."
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
@@ -9351,11 +9351,11 @@ msgstr "Has arribat al final del vostre cabal! Cerca alguns comptes més per a s
|
||||
|
||||
#: src/state/queries/video/video.ts:175
|
||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||
msgstr ""
|
||||
msgstr "Has assolit el teu límit diari de pujades de vídeos (massa bytes)"
|
||||
|
||||
#: src/state/queries/video/video.ts:180
|
||||
msgid "You've reached your daily limit for video uploads (too many videos)"
|
||||
msgstr ""
|
||||
msgstr "Has assolit el teu límit diari de pujades de vídeos (massa vídeos)"
|
||||
|
||||
#: src/screens/Signup/index.tsx:146
|
||||
msgid "Your account"
|
||||
|
||||
+243
-503
File diff suppressed because it is too large
Load Diff
+1820
-2689
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-07 20:28+0900\n"
|
||||
"PO-Revision-Date: 2024-09-14 17:17+0900\n"
|
||||
"Last-Translator: tkusano\n"
|
||||
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -532,7 +532,7 @@ msgstr "ビデオの圧縮中にエラーが発生しました。"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:315
|
||||
msgid "An error occurred while generating your starter pack. Want to try again?"
|
||||
msgstr "スターターパックの生成中にエラーが発生しました。再度試しますか?"
|
||||
msgstr "スターターパックの生成中にエラーが発生しました。もう一度試しますか?"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbed.tsx:135
|
||||
msgid "An error occurred while loading the video. Please try again later."
|
||||
@@ -544,7 +544,7 @@ msgstr "ビデオの読み込み時にエラーが発生しました。もう一
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
|
||||
msgid "An error occurred while saving the image!"
|
||||
msgstr ""
|
||||
msgstr "画像の保存中にエラーが発生しました!"
|
||||
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:71
|
||||
#: src/components/StarterPack/ShareDialog.tsx:79
|
||||
@@ -850,7 +850,7 @@ msgstr "Blueskyは友達と一緒のほうが楽しい!"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
|
||||
msgid "Bluesky now has over 10 million users, and I was #{0}!"
|
||||
msgstr "Bluesky のユーザー数は現在 1,000 万人を超えており、私は #{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."
|
||||
@@ -1099,14 +1099,6 @@ msgstr "確認コードが記載されたメールを確認し、ここに入力
|
||||
msgid "Check your inbox for an email with the confirmation code to enter below:"
|
||||
msgstr "入力したメールアドレスの受信トレイを確認して、以下に入力するための確認コードが記載されたメールが届いていないか確認してください:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:191
|
||||
#~ msgid "Choose 3 or more:"
|
||||
#~ msgstr "3つ以上選んでください:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:326
|
||||
#~ msgid "Choose at least {0} more"
|
||||
#~ msgstr "少なくともさらに{0}つ選んでください"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:190
|
||||
msgid "Choose Feeds"
|
||||
msgstr "フィードの選択"
|
||||
@@ -1818,10 +1810,6 @@ msgstr "新しいフィードを探す"
|
||||
msgid "Discover New Feeds"
|
||||
msgstr "新しいフィードを探す"
|
||||
|
||||
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
|
||||
#~ msgid "Dismiss"
|
||||
#~ msgstr "消す"
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1107
|
||||
msgid "Dismiss error"
|
||||
msgstr "エラーを消す"
|
||||
@@ -2073,7 +2061,7 @@ msgstr "メールアドレス"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "メール再送済"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2090,7 +2078,7 @@ msgstr "メールアドレスは認証されました"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "メールアドレス確認完了"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2210,7 +2198,7 @@ msgstr "ファイルの保存中にエラーが発生しました"
|
||||
|
||||
#: src/screens/Signup/StepCaptcha/index.tsx:56
|
||||
msgid "Error receiving captcha response."
|
||||
msgstr "Captchaレスポンスの受信中にエラーが発生しました。"
|
||||
msgstr "CAPTCHAレスポンスの受信中にエラーが発生しました。"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:197
|
||||
#: src/view/screens/Search/Search.tsx:116
|
||||
@@ -2350,7 +2338,7 @@ msgstr "スターターパックの作成に失敗しました"
|
||||
|
||||
#: src/view/com/modals/CreateOrEditList.tsx:194
|
||||
msgid "Failed to create the list. Check your internet connection and try again."
|
||||
msgstr "リストの作成に失敗しました。インターネットへの接続を確認の上、もう一度お試しください。"
|
||||
msgstr "リストの作成に失敗しました。インターネットへの接続を確認の上、もう一度試してください。"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:73
|
||||
msgid "Failed to delete message"
|
||||
@@ -2393,7 +2381,7 @@ msgstr "画像の保存に失敗しました:{0}"
|
||||
|
||||
#: src/state/queries/notifications/settings.ts:39
|
||||
msgid "Failed to save notification preferences, please try again"
|
||||
msgstr "通知の設定の保存に失敗しました。再度試してください"
|
||||
msgstr "通知の設定の保存に失敗しました。もう一度お試しください"
|
||||
|
||||
#: src/components/dms/MessageItem.tsx:225
|
||||
msgid "Failed to send"
|
||||
@@ -2402,11 +2390,11 @@ msgstr "送信に失敗"
|
||||
#: src/components/moderation/LabelsOnMeDialog.tsx:225
|
||||
#: src/screens/Messages/Conversation/ChatDisabled.tsx:87
|
||||
msgid "Failed to submit appeal, please try again."
|
||||
msgstr "異議申し立ての送信に失敗しました。再度試してください。"
|
||||
msgstr "異議申し立ての送信に失敗しました。もう一度お試しください。"
|
||||
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:223
|
||||
msgid "Failed to toggle thread mute, please try again"
|
||||
msgstr "スレッドのミュートの切り替えに失敗しました。再度試してください"
|
||||
msgstr "スレッドのミュートの切り替えに失敗しました。もう一度お試しください"
|
||||
|
||||
#: src/components/FeedCard.tsx:273
|
||||
msgid "Failed to update feeds"
|
||||
@@ -2554,16 +2542,16 @@ msgstr "アカウントをフォロー"
|
||||
msgid "Follow all"
|
||||
msgstr "すべてフォロー"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
msgid "Follow Back"
|
||||
msgstr "フォローバック"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr "フォローバック"
|
||||
|
||||
#: src/view/screens/Search/Explore.tsx:334
|
||||
msgid "Follow more accounts to get connected to your interests and build your network."
|
||||
msgstr "もっとたくさんのアカウントをフォローして、興味あることにつながり、ネットワークを広げましょう。"
|
||||
@@ -2922,7 +2910,7 @@ msgstr "そのモデレーションサービスを読み込めませんでした
|
||||
|
||||
#: src/state/queries/video/video.ts:165
|
||||
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
|
||||
msgstr ""
|
||||
msgstr "待って!徐々にビデオへのアクセスを許可していますが、あなたの順番はまだです。しばらくしてもう一度確認を!"
|
||||
|
||||
#: src/Navigation.tsx:550
|
||||
#: src/Navigation.tsx:570
|
||||
@@ -3088,7 +3076,7 @@ msgstr "無効なユーザー名またはパスワード"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "無効な確認コード"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:94
|
||||
msgid "Invite a Friend"
|
||||
@@ -3100,7 +3088,7 @@ msgstr "招待コード"
|
||||
|
||||
#: src/screens/Signup/state.ts:258
|
||||
msgid "Invite code not accepted. Check that you input it correctly and try again."
|
||||
msgstr "招待コードが確認できません。正しく入力されていることを確認し、もう一度実行してください。"
|
||||
msgstr "招待コードが確認できません。正しく入力されていることを確認し、もう一度試してください。"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:171
|
||||
msgid "Invite codes: {0} available"
|
||||
@@ -3147,7 +3135,7 @@ msgstr "会話に参加"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
|
||||
msgid "Joined {0}"
|
||||
msgstr "{0} 参加"
|
||||
msgstr "{0} に参加"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:21
|
||||
#: src/screens/Onboarding/state.ts:89
|
||||
@@ -3620,7 +3608,7 @@ msgstr "ミュート"
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
|
||||
msgctxt "video"
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
msgstr "ミュート"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:116
|
||||
msgid "Mute {truncatedTag}"
|
||||
@@ -3690,10 +3678,6 @@ msgstr "スレッドをミュート"
|
||||
msgid "Mute words & tags"
|
||||
msgstr "ワードとタグをミュート"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Muted"
|
||||
#~ msgstr "ミュートされています"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:264
|
||||
msgid "Muted accounts"
|
||||
msgstr "ミュート中のアカウント"
|
||||
@@ -4509,7 +4493,7 @@ msgstr "パスワードを選択してください。"
|
||||
|
||||
#: src/screens/Signup/state.ts:231
|
||||
msgid "Please complete the verification captcha."
|
||||
msgstr "Captcha認証を完了してください。"
|
||||
msgstr "CAPTCHA認証を完了してください。"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:65
|
||||
msgid "Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed."
|
||||
@@ -5205,11 +5189,11 @@ msgstr "メールを再送"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "メール再送済"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "確認メールを再送済"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -5797,10 +5781,6 @@ msgstr "バッジを表示"
|
||||
msgid "Show badge and filter from feeds"
|
||||
msgstr "バッジの表示とフィードからのフィルタリング"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:215
|
||||
#~ msgid "Show follows similar to {0}"
|
||||
#~ msgstr "{0}に似たおすすめのフォロー候補を表示"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
|
||||
msgid "Show hidden replies"
|
||||
msgstr "非表示の返信を表示"
|
||||
@@ -5958,7 +5938,7 @@ msgstr "スターターパックを使わずにサインアップ"
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:316
|
||||
msgid "Similar accounts"
|
||||
msgstr "類似のアカウント"
|
||||
msgstr "似ているアカウント"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:245
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:191
|
||||
@@ -6177,7 +6157,7 @@ msgstr "タップしてフルスクリーンに"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
|
||||
msgid "Tap to play or pause"
|
||||
msgstr ""
|
||||
msgstr "タップして再生または一時停止"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
|
||||
msgid "Tap to toggle sound"
|
||||
@@ -6211,7 +6191,7 @@ msgstr "もう少し教えて"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
|
||||
msgid "Ten Million"
|
||||
msgstr "1000万"
|
||||
msgstr "1,000万"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:90
|
||||
msgid "Terms"
|
||||
@@ -6252,7 +6232,7 @@ msgstr "最初の 1,000 万人のユーザーの 1 人になっていただき
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
|
||||
msgid "Thanks, you have successfully verified your email address."
|
||||
msgstr ""
|
||||
msgstr "ありがとう、メールアドレスの確認に成功しました。"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -6353,7 +6333,7 @@ msgstr "サービス規約は移動しました"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "入力された確認コードが正しくありません。正しいリンクを使用したかを確認するか、新しい確認コードを要求してください。"
|
||||
|
||||
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
|
||||
msgid "There is no time limit for account deactivation, come back any time."
|
||||
@@ -6362,17 +6342,17 @@ msgstr "アカウントの無効化に期限はありません。いつでも戻
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:117
|
||||
#: src/view/screens/ProfileFeed.tsx:545
|
||||
msgid "There was an an issue contacting the server, please check your internet connection and try again."
|
||||
msgstr "サーバーへの問い合わせ中に問題が発生しました。インターネットへの接続を確認の上、もう一度お試しください。"
|
||||
msgstr "サーバーへの問い合わせ中に問題が発生しました。インターネットへの接続を確認の上、もう一度試してください。"
|
||||
|
||||
#: src/view/com/posts/FeedErrorMessage.tsx:145
|
||||
msgid "There was an an issue removing this feed. Please check your internet connection and try again."
|
||||
msgstr "フィードの削除中に問題が発生しました。インターネットへの接続を確認の上、もう一度お試しください。"
|
||||
msgstr "フィードの削除中に問題が発生しました。インターネットへの接続を確認の上、もう一度試してください。"
|
||||
|
||||
#: src/view/com/posts/FeedShutdownMsg.tsx:52
|
||||
#: src/view/com/posts/FeedShutdownMsg.tsx:71
|
||||
#: src/view/screens/ProfileFeed.tsx:206
|
||||
msgid "There was an an issue updating your feeds, please check your internet connection and try again."
|
||||
msgstr "フィードの更新中に問題が発生しました。インターネットへの接続を確認の上、もう一度お試しください。"
|
||||
msgstr "フィードの更新中に問題が発生しました。インターネットへの接続を確認の上、もう一度試してください。"
|
||||
|
||||
#: src/components/dialogs/GifSelect.ios.tsx:197
|
||||
#: src/components/dialogs/GifSelect.tsx:213
|
||||
@@ -6816,7 +6796,7 @@ msgstr "ミュートを解除"
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
|
||||
msgctxt "video"
|
||||
msgid "Unmute"
|
||||
msgstr ""
|
||||
msgstr "ミュートを解除"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:115
|
||||
msgid "Unmute {truncatedTag}"
|
||||
@@ -6844,10 +6824,6 @@ msgstr "スレッドのミュートを解除"
|
||||
msgid "Unmute video"
|
||||
msgstr "ビデオのミュートを解除"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Unmuted"
|
||||
#~ msgstr "ミュート解除中"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:292
|
||||
#: src/view/screens/ProfileList.tsx:673
|
||||
msgid "Unpin"
|
||||
@@ -7075,7 +7051,7 @@ msgstr "メールアドレスを確認"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "メールアドレス確認ダイアログ"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -7135,7 +7111,7 @@ msgstr "ビデオ:{0}"
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
|
||||
msgid "Videos must be less than 60 seconds long"
|
||||
msgstr ""
|
||||
msgstr "ビデオは60秒より短くなくてはなりません"
|
||||
|
||||
#: src/screens/Profile/Header/Shell.tsx:113
|
||||
msgid "View {0}'s avatar"
|
||||
@@ -7248,7 +7224,7 @@ msgstr "あなたのアカウントが準備できるまで{estimatedTime}ほど
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "別の確認コードを<0>{0}</0>へ送りました。"
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -7260,7 +7236,7 @@ msgstr "あなたのフォロー中のユーザーの投稿を読み終わりま
|
||||
|
||||
#: src/state/queries/video/video.ts:170
|
||||
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
|
||||
msgstr ""
|
||||
msgstr "あなたがビデオのアップロードを許可されているかどうか判断できません。もう一度お試しください。"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:52
|
||||
msgid "We were unable to load your birth date preferences. Please try again."
|
||||
@@ -7284,7 +7260,7 @@ msgstr "これはあなたの体験をカスタマイズするために使用さ
|
||||
|
||||
#: src/components/dms/dialogs/SearchablePeopleList.tsx:90
|
||||
msgid "We're having network issues, try again"
|
||||
msgstr "ネットワークで問題が発生しています。再度試してください"
|
||||
msgstr "ネットワークで問題が発生しています。もう一度試してください"
|
||||
|
||||
#: src/screens/Signup/index.tsx:100
|
||||
msgid "We're so excited to have you join us!"
|
||||
@@ -7300,7 +7276,7 @@ msgstr "大変申し訳ありませんが、現在ミュートされたワード
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:206
|
||||
msgid "We're sorry, but your search could not be completed. Please try again in a few minutes."
|
||||
msgstr "大変申し訳ありませんが、検索を完了できませんでした。数分後に再試行してください。"
|
||||
msgstr "大変申し訳ありませんが、検索を完了できませんでした。数分後にもう一度お試しください。"
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:421
|
||||
msgid "We're sorry! The post you are replying to has been deleted."
|
||||
@@ -7464,7 +7440,7 @@ msgstr "あなたは並んでいます。"
|
||||
|
||||
#: src/state/queries/video/video.ts:161
|
||||
msgid "You are not allowed to upload videos."
|
||||
msgstr ""
|
||||
msgstr "あなたはビデオのアップロードを許可されていません。"
|
||||
|
||||
#: src/view/com/profile/ProfileFollows.tsx:95
|
||||
msgid "You are not following anyone."
|
||||
@@ -7585,7 +7561,7 @@ msgstr "最後まで到達しました"
|
||||
|
||||
#: src/state/queries/video/video-upload.shared.ts:67
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
msgstr "ビデオのアップロードの制限に一時的に到達しました。時間をおいてもう一度お試しください。"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
@@ -7711,11 +7687,11 @@ msgstr "フィードはここまでです!もっとフォローするアカウ
|
||||
|
||||
#: src/state/queries/video/video.ts:175
|
||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||
msgstr ""
|
||||
msgstr "ビデオのアップロードの一日の上限に到達しました(容量が大きすぎます)"
|
||||
|
||||
#: src/state/queries/video/video.ts:180
|
||||
msgid "You've reached your daily limit for video uploads (too many videos)"
|
||||
msgstr ""
|
||||
msgstr "ビデオのアップロードの一日の上限に到達しました(ビデオの数が多すぎます)"
|
||||
|
||||
#: src/screens/Signup/index.tsx:146
|
||||
msgid "Your account"
|
||||
@@ -7727,7 +7703,7 @@ msgstr "あなたのアカウントは削除されました"
|
||||
|
||||
#: src/state/queries/video/video.ts:185
|
||||
msgid "Your account is not yet old enough to upload videos. Please try again later."
|
||||
msgstr ""
|
||||
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."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ko\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-09-06 14:25+0900\n"
|
||||
"PO-Revision-Date: 2024-09-17 00:08+0900\n"
|
||||
"Last-Translator: quiple\n"
|
||||
"Language-Team: quiple, lens0021, HaruChanHeart, hazzzi, heartade\n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -544,7 +544,7 @@ msgstr "동영상을 불러오는 동안 오류가 발생했습니다. 다시
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
|
||||
msgid "An error occurred while saving the image!"
|
||||
msgstr ""
|
||||
msgstr "이미지를 저장하는 동안 오류가 발생했습니다"
|
||||
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:71
|
||||
#: src/components/StarterPack/ShareDialog.tsx:79
|
||||
@@ -850,7 +850,7 @@ msgstr "Bluesky는 친구와 함께하면 더 좋답니다!"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
|
||||
msgid "Bluesky now has over 10 million users, and I was #{0}!"
|
||||
msgstr ""
|
||||
msgstr "Bluesky는 현재 천만 명이 넘는 사용자를 보유하고 있으며, 나는 {0}번째였습니다!"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
|
||||
msgid "Bluesky will choose a set of recommended accounts from people in your network."
|
||||
@@ -873,9 +873,9 @@ msgstr "이미지 흐리게 및 피드에서 필터링"
|
||||
msgid "Books"
|
||||
msgstr "책"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
|
||||
msgid "Brag a little!"
|
||||
msgstr ""
|
||||
msgstr "자랑해 보세요!"
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:350
|
||||
msgid "Browse more accounts on the Explore page"
|
||||
@@ -1016,9 +1016,9 @@ msgstr "자막(.vtt)"
|
||||
msgid "Captions & alt text"
|
||||
msgstr "자막 및 대체 텍스트"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:369
|
||||
msgid "Celebrating {0} users"
|
||||
msgstr ""
|
||||
msgstr "사용자 {0}명 축하하기"
|
||||
|
||||
#: src/view/com/modals/VerifyEmail.tsx:160
|
||||
msgid "Change"
|
||||
@@ -1099,14 +1099,6 @@ msgstr "이메일에서 로그인 코드를 확인한 후 여기에 입력하세
|
||||
msgid "Check your inbox for an email with the confirmation code to enter below:"
|
||||
msgstr "받은 편지함에서 아래에 입력할 인증 코드가 포함된 이메일이 있는지 확인하세요."
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:191
|
||||
#~ msgid "Choose 3 or more:"
|
||||
#~ msgstr "3개 이상 선택하세요."
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:326
|
||||
#~ msgid "Choose at least {0} more"
|
||||
#~ msgstr "최소 {0}개 이상 선택하세요"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:190
|
||||
msgid "Choose Feeds"
|
||||
msgstr "피드 선택"
|
||||
@@ -1818,10 +1810,6 @@ msgstr "새 피드 발견하기"
|
||||
msgid "Discover New Feeds"
|
||||
msgstr "새 피드 발견하기"
|
||||
|
||||
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
|
||||
#~ msgid "Dismiss"
|
||||
#~ msgstr "닫기"
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1107
|
||||
msgid "Dismiss error"
|
||||
msgstr "오류 무시"
|
||||
@@ -1907,9 +1895,9 @@ msgstr "Bluesky 다운로드"
|
||||
msgid "Download CAR file"
|
||||
msgstr "CAR 파일 다운로드"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:629
|
||||
msgid "Download image"
|
||||
msgstr ""
|
||||
msgstr "이미지 다운로드"
|
||||
|
||||
#: src/view/com/composer/text-input/TextInput.web.tsx:269
|
||||
msgid "Drop to add images"
|
||||
@@ -2073,7 +2061,7 @@ msgstr "이메일 주소"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "이메일 다시 전송됨"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2086,11 +2074,11 @@ msgstr "이메일 변경됨"
|
||||
|
||||
#: src/view/com/modals/VerifyEmail.tsx:85
|
||||
msgid "Email verified"
|
||||
msgstr "이메일 확인됨"
|
||||
msgstr "이메일 인증됨"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "이메일 인증됨"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2152,7 +2140,7 @@ msgstr "피드 끝"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
|
||||
msgid "Ensure you have selected a language for each subtitle file."
|
||||
msgstr ""
|
||||
msgstr "각 자막 파일에 대한 언어를 선택했는지 확인하세요."
|
||||
|
||||
#: src/view/com/modals/AddAppPasswords.tsx:161
|
||||
msgid "Enter a name for this App Password"
|
||||
@@ -2554,16 +2542,16 @@ msgstr "계정 팔로우"
|
||||
msgid "Follow all"
|
||||
msgstr "모두 팔로우"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
msgid "Follow Back"
|
||||
msgstr "맞팔로우"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr "맞팔로우"
|
||||
|
||||
#: src/view/screens/Search/Explore.tsx:334
|
||||
msgid "Follow more accounts to get connected to your interests and build your network."
|
||||
msgstr "더 많은 계정을 팔로우하고 관심 분야를 연결하여 네트워크를 구축하세요."
|
||||
@@ -2922,7 +2910,7 @@ msgstr "검토 서비스를 불러올 수 없습니다."
|
||||
|
||||
#: src/state/queries/video/video.ts:165
|
||||
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
|
||||
msgstr ""
|
||||
msgstr "잠깐! 동영상에 대한 접근 권한이 점차적으로 제공되고 있지만 아직은 기다려야 합니다. 나중에 다시 확인해 주세요!"
|
||||
|
||||
#: src/Navigation.tsx:550
|
||||
#: src/Navigation.tsx:570
|
||||
@@ -2977,7 +2965,7 @@ msgstr "아무것도 선택하지 않으면 모든 연령대에 적합하다는
|
||||
|
||||
#: src/screens/Signup/StepInfo/Policies.tsx:89
|
||||
msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf."
|
||||
msgstr "해당 국가의 법률에 따라 아직 성인이 아닌 경우, 부모 또는 법적 보호자가 대신 이 약관을 읽어야 합니다."
|
||||
msgstr "해당 국가의 법률에 따라 아직 성인이 아닌 경우 부모 또는 법적 보호자가 대신 이 약관을 읽어야 합니다."
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:720
|
||||
msgid "If you delete this list, you won't be able to recover it."
|
||||
@@ -3088,7 +3076,7 @@ msgstr "잘못된 사용자 이름 또는 비밀번호"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "잘못된 인증 코드"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:94
|
||||
msgid "Invite a Friend"
|
||||
@@ -3145,9 +3133,9 @@ msgstr "Bluesky 가입하기"
|
||||
msgid "Join the conversation"
|
||||
msgstr "대화에 참여하기"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:498
|
||||
msgid "Joined {0}"
|
||||
msgstr ""
|
||||
msgstr "{0}에 가입함"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:21
|
||||
#: src/screens/Onboarding/state.ts:89
|
||||
@@ -3612,15 +3600,15 @@ msgstr "영화"
|
||||
msgid "Music"
|
||||
msgstr "음악"
|
||||
|
||||
#: src/components/TagMenu/index.tsx:263
|
||||
msgid "Mute"
|
||||
msgstr "뮤트"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:166
|
||||
#: 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/VideoEmbedInnerNative.tsx:163
|
||||
msgid "Mute"
|
||||
msgstr "뮤트"
|
||||
msgstr "음소거"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:116
|
||||
msgid "Mute {truncatedTag}"
|
||||
@@ -3690,10 +3678,6 @@ msgstr "스레드 뮤트"
|
||||
msgid "Mute words & tags"
|
||||
msgstr "단어 및 태그 뮤트"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Muted"
|
||||
#~ msgstr "뮤트됨"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:264
|
||||
msgid "Muted accounts"
|
||||
msgstr "뮤트한 계정"
|
||||
@@ -3906,6 +3890,11 @@ msgstr "인기 GIF를 찾을 수 없습니다. Tenor에 문제가 있을 수 있
|
||||
msgid "No feeds found. Try searching for something else."
|
||||
msgstr "피드를 찾을 수 없습니다. 다른 피드를 검색해 보세요."
|
||||
|
||||
#: src/components/LikedByList.tsx:78
|
||||
#: src/view/com/post-thread/PostLikedBy.tsx:85
|
||||
msgid "No likes yet"
|
||||
msgstr "아직 좋아요 없음"
|
||||
|
||||
#: src/components/ProfileCard.tsx:336
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:116
|
||||
msgid "No longer following {0}"
|
||||
@@ -3925,7 +3914,7 @@ msgstr "더 이상 표시할 대화가 없습니다"
|
||||
|
||||
#: src/view/com/notifications/Feed.tsx:121
|
||||
msgid "No notifications yet!"
|
||||
msgstr "아직 알림이 없습니다."
|
||||
msgstr "아직 알림이 없습니다"
|
||||
|
||||
#: src/components/dms/MessagesNUX.tsx:149
|
||||
#: src/components/dms/MessagesNUX.tsx:152
|
||||
@@ -3942,6 +3931,14 @@ msgstr "이 게시물은 작성자 외에는 누구도 인용할 수 없습니
|
||||
msgid "No posts yet."
|
||||
msgstr "아직 게시물이 없습니다."
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:106
|
||||
msgid "No quotes yet"
|
||||
msgstr "아직 인용 없음"
|
||||
|
||||
#: src/view/com/post-thread/PostRepostedBy.tsx:78
|
||||
msgid "No reposts yet"
|
||||
msgstr "아직 재게시 없음"
|
||||
|
||||
#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:101
|
||||
#: src/view/com/composer/text-input/web/Autocomplete.tsx:195
|
||||
msgid "No result"
|
||||
@@ -3980,11 +3977,20 @@ msgstr "사용하지 않음"
|
||||
msgid "Nobody"
|
||||
msgstr "없음"
|
||||
|
||||
#: src/components/LikedByList.tsx:79
|
||||
#: src/components/LikedByList.tsx:80
|
||||
#: src/components/LikesDialog.tsx:99
|
||||
#: src/view/com/post-thread/PostLikedBy.tsx:87
|
||||
msgid "Nobody has liked this yet. Maybe you should be the first!"
|
||||
msgstr "아직 아무도 좋아요를 누르지 않았습니다. 첫 번째가 되어 보세요!"
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:108
|
||||
msgid "Nobody has quoted this yet. Maybe you should be the first!"
|
||||
msgstr "아직 아무도 인용하지 않았습니다. 첫 번째가 되어 보세요!"
|
||||
|
||||
#: src/view/com/post-thread/PostRepostedBy.tsx:80
|
||||
msgid "Nobody has reposted this yet. Maybe you should be the first!"
|
||||
msgstr "아직 아무도 재게시하지 않았습니다. 첫 번째가 되어 보세요!"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/StepProfiles.tsx:103
|
||||
msgid "Nobody was found. Try searching for someone else."
|
||||
msgstr "아무도 찾을 수 없습니다. 다른 사용자를 검색해 보세요."
|
||||
@@ -4081,7 +4087,7 @@ msgstr "이런! 뭔가 잘못되었습니다."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
|
||||
msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋"
|
||||
msgstr ""
|
||||
msgstr "이런! 공유할 이미지를 생성할 수 없습니다. 안심하고 방문해 주셔서 감사합니다."
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339
|
||||
msgid "OK"
|
||||
@@ -5201,15 +5207,15 @@ msgstr "이 제공자에서 필수"
|
||||
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:168
|
||||
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:171
|
||||
msgid "Resend email"
|
||||
msgstr "이메일 다시 보내기"
|
||||
msgstr "이메일 다시 전송"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "이메일 다시 전송"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "인증 이메일 다시 전송하기"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -5686,7 +5692,7 @@ msgstr "성행위 또는 선정적인 노출."
|
||||
msgid "Sexually Suggestive"
|
||||
msgstr "외설적"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:644
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:651
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:177
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:411
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:582
|
||||
@@ -5723,13 +5729,13 @@ msgstr "무시하고 공유"
|
||||
msgid "Share feed"
|
||||
msgstr "피드 공유"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:628
|
||||
msgid "Share image externally"
|
||||
msgstr ""
|
||||
msgstr "외부로 이미지 공유"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:646
|
||||
msgid "Share image in post"
|
||||
msgstr ""
|
||||
msgstr "게시물로 이미지 공유"
|
||||
|
||||
#: src/components/StarterPack/ShareDialog.tsx:124
|
||||
#: src/components/StarterPack/ShareDialog.tsx:131
|
||||
@@ -5797,10 +5803,6 @@ msgstr "배지 표시"
|
||||
msgid "Show badge and filter from feeds"
|
||||
msgstr "배지 표시 및 피드에서 필터링"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
|
||||
#~ msgid "Show follows similar to {0}"
|
||||
#~ msgstr "{0} 님과 비슷한 팔로우 표시"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
|
||||
msgid "Show hidden replies"
|
||||
msgstr "숨겨진 답글 표시"
|
||||
@@ -6177,9 +6179,9 @@ msgstr "탭하여 전체화면으로 보기"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
|
||||
msgid "Tap to play or pause"
|
||||
msgstr ""
|
||||
msgstr "탭하여 재생/일시 정지"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
msgid "Tap to toggle sound"
|
||||
msgstr "탭하여 소리 켜기/끄기"
|
||||
|
||||
@@ -6209,9 +6211,9 @@ msgstr "농담해 보세요!"
|
||||
msgid "Tell us a little more"
|
||||
msgstr "좀 더 자세히 알려주세요"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:524
|
||||
msgid "Ten Million"
|
||||
msgstr ""
|
||||
msgstr "천만 명"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:90
|
||||
msgid "Terms"
|
||||
@@ -6246,13 +6248,13 @@ msgstr "텍스트 입력 필드"
|
||||
msgid "Thank you. Your report has been sent."
|
||||
msgstr "감사합니다. 신고를 전송했습니다."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:593
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:600
|
||||
msgid "Thanks for being one of our first 10 million users."
|
||||
msgstr ""
|
||||
msgstr "사용자 천만 명 중 한 명이 되어 주셔서 감사합니다."
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
|
||||
msgid "Thanks, you have successfully verified your email address."
|
||||
msgstr ""
|
||||
msgstr "이메일 주소를 성공적으로 인증했습니다."
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -6271,7 +6273,7 @@ msgstr "이 핸들은 이미 사용 중입니다."
|
||||
msgid "That starter pack could not be found."
|
||||
msgstr "스타터 팩을 찾을 수 없습니다."
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:127
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:133
|
||||
msgid "That's all, folks!"
|
||||
msgstr "이상입니다, 여러분!"
|
||||
|
||||
@@ -6353,7 +6355,7 @@ msgstr "서비스 이용약관을 다음으로 이동했습니다:"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "입력한 인증 코드가 올바르지 않습니다. 올바른 인증 링크를 사용했는지 확인하거나 새 인증 링크를 요청하세요."
|
||||
|
||||
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
|
||||
msgid "There is no time limit for account deactivation, come back any time."
|
||||
@@ -6680,9 +6682,9 @@ msgstr "Bluesky에 동영상을 업로드하려면 먼저 이메일을 인증해
|
||||
msgid "To whom would you like to send this report?"
|
||||
msgstr "이 신고를 누구에게 보내시겠습니까?"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:597
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:604
|
||||
msgid "Together, we're rebuilding the social internet. We're glad you're here."
|
||||
msgstr ""
|
||||
msgstr "우리는 함께 소셜 인터넷을 재건하고 있습니다. 함께해 주셔서 감사합니다."
|
||||
|
||||
#: src/view/com/util/forms/DropdownButton.tsx:255
|
||||
msgid "Toggle dropdown"
|
||||
@@ -6807,17 +6809,17 @@ 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/VideoEmbedInnerNative.tsx:163
|
||||
#: src/view/screens/ProfileList.tsx:689
|
||||
msgid "Unmute"
|
||||
msgstr "언뮤트"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:165
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
|
||||
msgctxt "video"
|
||||
msgid "Unmute"
|
||||
msgstr "음소거 해제"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:115
|
||||
msgid "Unmute {truncatedTag}"
|
||||
msgstr "{truncatedTag} 언뮤트"
|
||||
@@ -6844,10 +6846,6 @@ msgstr "스레드 언뮤트"
|
||||
msgid "Unmute video"
|
||||
msgstr "동영상 음소거 해제"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Unmuted"
|
||||
#~ msgstr "음소거 해제됨"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:292
|
||||
#: src/view/screens/ProfileList.tsx:673
|
||||
msgid "Unpin"
|
||||
@@ -7075,7 +7073,7 @@ msgstr "이메일 인증"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "이메일 인증 대화 상자"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -7135,11 +7133,11 @@ msgstr "동영상: {0}"
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
|
||||
msgid "Videos must be less than 60 seconds long"
|
||||
msgstr ""
|
||||
msgstr "동영상 길이는 60초 미만이어야 합니다."
|
||||
|
||||
#: src/screens/Profile/Header/Shell.tsx:113
|
||||
msgid "View {0}'s avatar"
|
||||
msgstr "{0} 님의 아바타를 봅니다"
|
||||
msgstr "{0} 님의 아바타 보기"
|
||||
|
||||
#: src/components/ProfileCard.tsx:110
|
||||
#: src/view/com/notifications/FeedItem.tsx:273
|
||||
@@ -7248,7 +7246,7 @@ msgstr "계정이 준비될 때까지 {estimatedTime}이(가) 걸릴 것으로
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "<0>{0}</0>(으)로 또 다른 인증 이메일을 보냈습니다."
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -7260,7 +7258,7 @@ msgstr "팔로우한 사용자의 게시물이 부족합니다. 대신 <0/>의
|
||||
|
||||
#: src/state/queries/video/video.ts:170
|
||||
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
|
||||
msgstr ""
|
||||
msgstr "동영상을 업로드할 수 있는지 확인할 수 없습니다. 다시 시도해 주세요."
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:52
|
||||
msgid "We were unable to load your birth date preferences. Please try again."
|
||||
@@ -7464,7 +7462,7 @@ msgstr "대기 중입니다."
|
||||
|
||||
#: src/state/queries/video/video.ts:161
|
||||
msgid "You are not allowed to upload videos."
|
||||
msgstr ""
|
||||
msgstr "동영상을 업로드할 수 없습니다."
|
||||
|
||||
#: src/view/com/profile/ProfileFollows.tsx:95
|
||||
msgid "You are not following anyone."
|
||||
@@ -7585,7 +7583,7 @@ msgstr "끝에 도달했습니다"
|
||||
|
||||
#: src/state/queries/video/video-upload.shared.ts:67
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
msgstr "일시적으로 동영상 업로드 한도에 도달했습니다. 나중에 다시 시도해 주세요."
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
@@ -7711,11 +7709,11 @@ msgstr "피드 끝에 도달했습니다! 팔로우할 계정을 더 찾아보
|
||||
|
||||
#: src/state/queries/video/video.ts:175
|
||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||
msgstr ""
|
||||
msgstr "동영상 업로드 일일 한도에 도달했습니다 (용량이 너무 큼)"
|
||||
|
||||
#: src/state/queries/video/video.ts:180
|
||||
msgid "You've reached your daily limit for video uploads (too many videos)"
|
||||
msgstr ""
|
||||
msgstr "동영상 업로드 일일 한도에 도달했습니다 (동영상 수가 너무 많음)"
|
||||
|
||||
#: src/screens/Signup/index.tsx:146
|
||||
msgid "Your account"
|
||||
@@ -7727,7 +7725,7 @@ msgstr "계정을 삭제했습니다"
|
||||
|
||||
#: src/state/queries/video/video.ts:185
|
||||
msgid "Your account is not yet old enough to upload videos. Please try again later."
|
||||
msgstr ""
|
||||
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."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pt-BR\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-05-13 11:41\n"
|
||||
"PO-Revision-Date: 2024-09-16 17:30+01:00\n"
|
||||
"Last-Translator: fabiohcnobre\n"
|
||||
"Language-Team: maisondasilva, MightyLoggor, gildaswise, gleydson, faeriarum, fabiohcnobre, garccez\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
@@ -28,11 +28,11 @@ msgstr "{0, plural, one {{formattedCount} outro} other {{formattedCount} outros}
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:156
|
||||
msgid "{0, plural, one {# day} other {# days}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# dia} other {# dias}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:146
|
||||
msgid "{0, plural, one {# hour} other {# hours}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# hora} other {# horas}}"
|
||||
|
||||
#: src/components/moderation/LabelsOnMe.tsx:55
|
||||
#~ msgid "{0, plural, one {# label has been placed on this account} other {# labels has been placed on this account}}"
|
||||
@@ -52,11 +52,11 @@ msgstr "{0, plural, one {# rótulo foi colocado neste conteúdo} other {# rótul
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:136
|
||||
msgid "{0, plural, one {# minute} other {# minutes}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# minuto} other {# minutos}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:167
|
||||
msgid "{0, plural, one {# month} other {# months}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# mês} other {# meses}}"
|
||||
|
||||
#: src/view/com/util/post-ctrls/RepostButton.tsx:71
|
||||
msgid "{0, plural, one {# repost} other {# reposts}}"
|
||||
@@ -64,7 +64,7 @@ msgstr "{0, plural, one {# repostagem} other {# repostagens}}"
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:126
|
||||
msgid "{0, plural, one {# second} other {# seconds}}"
|
||||
msgstr ""
|
||||
msgstr "{0, plural, one {# segundo} other {# segundos}}"
|
||||
|
||||
#: src/components/KnownFollowers.tsx:179
|
||||
#~ msgid "{0, plural, one {and # other} other {and # others}}"
|
||||
@@ -116,20 +116,20 @@ msgstr "{0, plural, one {Descurtir (# curtida)} other {Descurtir (# curtidas)}}"
|
||||
#. Pattern: {wordValue} in tags
|
||||
#: src/components/dialogs/MutedWords.tsx:475
|
||||
msgid "{0} <0>in <1>tags</1></0>"
|
||||
msgstr ""
|
||||
msgstr "{0} <0>em <1>tags</1></0>"
|
||||
|
||||
#. Pattern: {wordValue} in text, tags
|
||||
#: src/components/dialogs/MutedWords.tsx:465
|
||||
msgid "{0} <0>in <1>text & tags</1></0>"
|
||||
msgstr ""
|
||||
msgstr "{0} <0>em <1>texto e tags</1></0>"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackLandingScreen.tsx:228
|
||||
msgid "{0} joined this week"
|
||||
msgstr "{0} entrou esta semana"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:637
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:195
|
||||
msgid "{0} of {1}"
|
||||
msgstr ""
|
||||
msgstr "{0} de {1}"
|
||||
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:467
|
||||
msgid "{0} people have used this starter pack!"
|
||||
@@ -154,27 +154,27 @@ msgstr "O pacote inicial de {0}"
|
||||
#. How many days have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:158
|
||||
msgid "{0}d"
|
||||
msgstr ""
|
||||
msgstr "{0}d"
|
||||
|
||||
#. How many hours have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:148
|
||||
msgid "{0}h"
|
||||
msgstr ""
|
||||
msgstr "{0}h"
|
||||
|
||||
#. How many minutes have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:138
|
||||
msgid "{0}m"
|
||||
msgstr ""
|
||||
msgstr "{0}m"
|
||||
|
||||
#. How many months have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:169
|
||||
msgid "{0}mo"
|
||||
msgstr ""
|
||||
msgstr "{0}me"
|
||||
|
||||
#. How many seconds have passed, displayed in a narrow form
|
||||
#: src/lib/hooks/useTimeAgo.ts:128
|
||||
msgid "{0}s"
|
||||
msgstr ""
|
||||
msgstr "{0}s"
|
||||
|
||||
#: src/components/LabelingServiceCard/index.tsx:71
|
||||
msgid "{count, plural, one {Liked by # user} other {Liked by # users}}"
|
||||
@@ -343,12 +343,16 @@ msgstr "7 dias"
|
||||
#~ msgid "A help tooltip"
|
||||
#~ msgstr "Uma sugestão de ajuda"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:266
|
||||
msgid "A virtual certificate with text \"Celebrating 10M users on Bluesky, #{0}, {displayName} {handle}, joined on {joinedDate}\""
|
||||
msgstr "Um certificado virtual com o texto \"Comemorando 10 milhões de usuários no Bluesky, #{0}, {displayName} {handle}, ingressou em {joinedDate}\""
|
||||
|
||||
#: src/view/com/util/ViewHeader.tsx:92
|
||||
#: src/view/screens/Search/Search.tsx:684
|
||||
msgid "Access navigation links and settings"
|
||||
msgstr "Acessar links de navegação e configurações"
|
||||
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:56
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:103
|
||||
msgid "Access profile and other navigation links"
|
||||
msgstr "Acessar perfil e outros links de navegação"
|
||||
|
||||
@@ -640,7 +644,7 @@ msgstr "Ocorreu um erro"
|
||||
#~ msgid "An error occured"
|
||||
#~ msgstr "Tivemos um problema"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:413
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:416
|
||||
msgid "An error occurred"
|
||||
msgstr "Ocorreu um erro"
|
||||
|
||||
@@ -660,13 +664,9 @@ msgstr "Ocorreu um erro ao carregar o vídeo. Tente novamente mais tarde."
|
||||
msgid "An error occurred while loading the video. Please try again."
|
||||
msgstr "Ocorreu um erro ao carregar o vídeo. Tente novamente."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:315
|
||||
msgid "An error occurred while saving the image!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ShareDialog.tsx:79
|
||||
#~ msgid "An error occurred while saving the image."
|
||||
#~ msgstr "Ocorreu um erro ao salvar a imagem."
|
||||
msgstr "Ocorreu um erro ao salvar a imagem!"
|
||||
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:71
|
||||
#: src/components/StarterPack/ShareDialog.tsx:79
|
||||
@@ -994,6 +994,11 @@ msgstr "Bluesky é uma rede aberta que permite a escolha do seu provedor de hosp
|
||||
msgid "Bluesky is better with friends!"
|
||||
msgstr "Bluesky é melhor com amigos!"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:43
|
||||
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:59
|
||||
msgid "Bluesky is celebrating 10 million users!"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/auth/onboarding/WelcomeDesktop.tsx:80
|
||||
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:82
|
||||
#~ msgid "Bluesky is flexible."
|
||||
@@ -1009,9 +1014,9 @@ msgstr "Bluesky é melhor com amigos!"
|
||||
#~ msgid "Bluesky is public."
|
||||
#~ msgstr "Bluesky é público."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:256
|
||||
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}!"
|
||||
msgstr "O Bluesky agora tem mais de 10 milhões de usuários, e eu fui o #{0}!"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
|
||||
msgid "Bluesky will choose a set of recommended accounts from people in your network."
|
||||
@@ -1034,7 +1039,7 @@ msgstr "Desfocar imagens e filtrar dos feeds"
|
||||
msgid "Books"
|
||||
msgstr "Livros"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:677
|
||||
msgid "Brag a little!"
|
||||
msgstr "Gabe-se um pouco!"
|
||||
|
||||
@@ -1104,8 +1109,8 @@ msgid "Can only contain letters, numbers, spaces, dashes, and underscores. Must
|
||||
msgstr "Só pode conter letras, números, espaços, riscas e subtraços. Deve ter pelo menos 4 caracteres, mas não mais de 32 caracteres."
|
||||
|
||||
#: src/components/Menu/index.tsx:235
|
||||
#: src/components/Prompt.tsx:122
|
||||
#: src/components/Prompt.tsx:124
|
||||
#: src/components/Prompt.tsx:126
|
||||
#: src/components/TagMenu/index.tsx:282
|
||||
#: src/screens/Deactivated.tsx:161
|
||||
#: src/view/com/composer/Composer.tsx:594
|
||||
@@ -1175,7 +1180,7 @@ msgstr "Cancela a abertura do link"
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:155
|
||||
#: src/view/com/util/post-ctrls/PostCtrls.tsx:191
|
||||
msgid "Cannot interact with a blocked user"
|
||||
msgstr ""
|
||||
msgstr "Não é possível interagir com um usuário bloqueado"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:135
|
||||
msgid "Captions (.vtt)"
|
||||
@@ -1185,7 +1190,7 @@ msgstr "Legendas (.vtt)"
|
||||
msgid "Captions & alt text"
|
||||
msgstr "Legendas e texto alt"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:434
|
||||
msgid "Celebrating {0} users"
|
||||
msgstr "Comemorando {0} usuários"
|
||||
|
||||
@@ -1536,8 +1541,8 @@ msgstr "Configure o filtro de conteúdo por categoria: {name}"
|
||||
msgid "Configured in <0>moderation settings</0>."
|
||||
msgstr "Configure no <0>painel de moderação</0>."
|
||||
|
||||
#: src/components/Prompt.tsx:165
|
||||
#: src/components/Prompt.tsx:168
|
||||
#: src/components/Prompt.tsx:167
|
||||
#: src/components/Prompt.tsx:170
|
||||
#: src/view/com/modals/SelfLabel.tsx:155
|
||||
#: src/view/com/modals/VerifyEmail.tsx:239
|
||||
#: src/view/com/modals/VerifyEmail.tsx:241
|
||||
@@ -2054,7 +2059,7 @@ msgstr "Desabilitar feedback tátil"
|
||||
#~ msgid "Disable haptics"
|
||||
#~ msgstr "Desabilitar feedback tátil"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:379
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:382
|
||||
msgid "Disable subtitles"
|
||||
msgstr "Desativar legendas"
|
||||
|
||||
@@ -2190,7 +2195,7 @@ msgstr "Baixe o Bluesky"
|
||||
msgid "Download CAR file"
|
||||
msgstr "Baixar arquivo CAR"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:686
|
||||
msgid "Download image"
|
||||
msgstr "Baixar imagem"
|
||||
|
||||
@@ -2369,7 +2374,7 @@ msgstr "Endereço de e-mail"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "E-mail Reenviado"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2386,7 +2391,7 @@ msgstr "E-mail verificado"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "E-mail Verificado"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2437,7 +2442,7 @@ msgstr "Habilitar mídia para"
|
||||
msgid "Enable priority notifications"
|
||||
msgstr "Habilitar notificações prioritárias"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:380
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:383
|
||||
msgid "Enable subtitles"
|
||||
msgstr "Habilitar legendas"
|
||||
|
||||
@@ -2569,7 +2574,7 @@ msgstr "Excluir usuário que você segue"
|
||||
msgid "Excludes users you follow"
|
||||
msgstr "Excluir usuário que você segue"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:397
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:400
|
||||
msgid "Exit fullscreen"
|
||||
msgstr "Sair da tela cheia"
|
||||
|
||||
@@ -2612,7 +2617,7 @@ msgstr "Experimental: Quando essa preferência estiver habilitada, você receber
|
||||
|
||||
#: src/components/dialogs/MutedWords.tsx:500
|
||||
msgid "Expired"
|
||||
msgstr "Expirado "
|
||||
msgstr "Expirado"
|
||||
|
||||
#: src/components/dialogs/MutedWords.tsx:502
|
||||
msgid "Expires {0}"
|
||||
@@ -2740,7 +2745,7 @@ msgstr "Falha ao alternar o silenciamento do tópico, tente novamente"
|
||||
|
||||
#: src/components/FeedCard.tsx:273
|
||||
msgid "Failed to update feeds"
|
||||
msgstr "Falha ao atualizar os feeds\""
|
||||
msgstr "Falha ao atualizar os feeds"
|
||||
|
||||
#: src/components/dms/MessagesNUX.tsx:60
|
||||
#: src/screens/Messages/Settings.tsx:35
|
||||
@@ -2916,18 +2921,14 @@ msgstr "Seguir Conta"
|
||||
msgid "Follow all"
|
||||
msgstr "Siga todos"
|
||||
|
||||
#: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:187
|
||||
#~ msgid "Follow All"
|
||||
#~ msgstr "Seguir Todas"
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
msgid "Follow Back"
|
||||
msgstr "Seguir De Volta"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
msgid "Follow Back"
|
||||
msgstr "Seguir De Volta"
|
||||
|
||||
#: src/view/screens/Search/Explore.tsx:334
|
||||
@@ -3083,7 +3084,7 @@ msgctxt "from-feed"
|
||||
msgid "From <0/>"
|
||||
msgstr "Por <0/>"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:398
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:401
|
||||
msgid "Fullscreen"
|
||||
msgstr "Tela cheia"
|
||||
|
||||
@@ -3114,7 +3115,7 @@ msgstr "Começando"
|
||||
|
||||
#: src/components/MediaPreview.tsx:122
|
||||
msgid "GIF"
|
||||
msgstr ""
|
||||
msgstr "GIF"
|
||||
|
||||
#: src/screens/Onboarding/StepProfile/index.tsx:225
|
||||
msgid "Give your profile a face"
|
||||
@@ -3427,7 +3428,7 @@ msgstr "Imagem"
|
||||
msgid "Image alt text"
|
||||
msgstr "Texto alternativo da imagem"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:247
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:312
|
||||
#: src/components/StarterPack/ShareDialog.tsx:76
|
||||
msgid "Image saved to your camera roll!"
|
||||
msgstr "Imagem salva no rolo da câmera!"
|
||||
@@ -3438,7 +3439,7 @@ msgstr "Falsificação de identidade ou alegações falsas sobre identidade ou f
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:68
|
||||
msgid "Impersonation, misinformation, or false claims"
|
||||
msgstr ""
|
||||
msgstr "Falsificação de identidade, desinformação ou alegações falsas"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:91
|
||||
msgid "Inappropriate messages or explicit links"
|
||||
@@ -3512,7 +3513,7 @@ msgstr "Credenciais inválidas"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "Código de Verificação Inválido"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:94
|
||||
msgid "Invite a Friend"
|
||||
@@ -3556,7 +3557,7 @@ msgstr "É só você por enquanto! Adicione mais pessoas ao seu pacote inicial p
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1126
|
||||
msgid "Job ID: {0}"
|
||||
msgstr ""
|
||||
msgstr "Job ID: {0}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:164
|
||||
msgid "Jobs"
|
||||
@@ -3574,8 +3575,12 @@ msgid "Join the conversation"
|
||||
msgstr "Participe da conversa"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
|
||||
msgid "Joined {0}"
|
||||
msgstr "Juntou-se {0}"
|
||||
#~ msgid "Joined {0}"
|
||||
#~ msgstr "Juntou-se {0}"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:559
|
||||
msgid "Joined on {joinedDate}"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:21
|
||||
#: src/screens/Onboarding/state.ts:89
|
||||
@@ -3986,7 +3991,7 @@ msgstr "Conta Enganosa"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:67
|
||||
msgid "Misleading Post"
|
||||
msgstr ""
|
||||
msgstr "Postagem Enganosa"
|
||||
|
||||
#: src/screens/Settings/AppearanceSettings.tsx:78
|
||||
msgid "Mode"
|
||||
@@ -4079,14 +4084,14 @@ msgid "Music"
|
||||
msgstr "Música"
|
||||
|
||||
#: src/components/TagMenu/index.tsx:263
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
|
||||
msgid "Mute"
|
||||
msgstr "Silenciar"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:157
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:94
|
||||
msgctxt "video"
|
||||
msgid "Mute"
|
||||
msgstr ""
|
||||
msgstr "Silenciar"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:116
|
||||
msgid "Mute {truncatedTag}"
|
||||
@@ -4395,6 +4400,11 @@ msgstr "Nenhum GIF em destaque encontrado."
|
||||
msgid "No feeds found. Try searching for something else."
|
||||
msgstr "Nenhum feed encontrado. Tente pesquisar por outra coisa."
|
||||
|
||||
#: src/components/LikedByList.tsx:78
|
||||
#: src/view/com/post-thread/PostLikedBy.tsx:85
|
||||
msgid "No likes yet"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ProfileCard.tsx:336
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:116
|
||||
msgid "No longer following {0}"
|
||||
@@ -4431,6 +4441,14 @@ msgstr "Ninguém além do autor pode citar esta postagem."
|
||||
msgid "No posts yet."
|
||||
msgstr "Nenhuma postagem ainda."
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:106
|
||||
msgid "No quotes yet"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/post-thread/PostRepostedBy.tsx:78
|
||||
msgid "No reposts yet"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/text-input/mobile/Autocomplete.tsx:101
|
||||
#: src/view/com/composer/text-input/web/Autocomplete.tsx:195
|
||||
msgid "No result"
|
||||
@@ -4477,11 +4495,20 @@ msgstr "Ninguém"
|
||||
#~ msgid "Nobody can reply"
|
||||
#~ msgstr "Ninguém pode responder"
|
||||
|
||||
#: src/components/LikedByList.tsx:79
|
||||
#: src/components/LikedByList.tsx:80
|
||||
#: src/components/LikesDialog.tsx:99
|
||||
#: src/view/com/post-thread/PostLikedBy.tsx:87
|
||||
msgid "Nobody has liked this yet. Maybe you should be the first!"
|
||||
msgstr "Ninguém curtiu isso ainda. Você pode ser o primeiro!"
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:108
|
||||
msgid "Nobody has quoted this yet. Maybe you should be the first!"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/post-thread/PostRepostedBy.tsx:80
|
||||
msgid "Nobody has reposted this yet. Maybe you should be the first!"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/StarterPack/Wizard/StepProfiles.tsx:103
|
||||
msgid "Nobody was found. Try searching for someone else."
|
||||
msgstr "Ninguém foi encontrado. Tente procurar por outra pessoa."
|
||||
@@ -4584,7 +4611,7 @@ msgstr "Opa!"
|
||||
msgid "Oh no! Something went wrong."
|
||||
msgstr "Opa! Algo deu errado."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:224
|
||||
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 🦋"
|
||||
|
||||
@@ -4600,17 +4627,9 @@ msgstr "Ok"
|
||||
msgid "Oldest replies first"
|
||||
msgstr "Respostas mais antigas primeiro"
|
||||
|
||||
#: src/components/StarterPack/QrCode.tsx:69
|
||||
#~ msgid "on"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/hooks/useTimeAgo.ts:81
|
||||
#~ msgid "on {str}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/StarterPack/QrCode.tsx:70
|
||||
msgid "on<0><1/><2><3/></2></0>"
|
||||
msgstr ""
|
||||
msgstr "no<0><1/><2><3/></2></0>"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:226
|
||||
msgid "Onboarding reset"
|
||||
@@ -4640,7 +4659,7 @@ msgstr "Apenas {0} pode responder."
|
||||
msgid "Only contains letters, numbers, and hyphens"
|
||||
msgstr "Contém apenas letras, números e hífens"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:39
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:40
|
||||
msgid "Only WebVTT (.vtt) files are supported"
|
||||
msgstr "Somente arquivos WebVTT (.vtt) são suportados"
|
||||
|
||||
@@ -4696,7 +4715,7 @@ msgstr "Abrir opções de mensagem"
|
||||
msgid "Open muted words and tags settings"
|
||||
msgstr "Abrir opções de palavras/tags silenciadas"
|
||||
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:54
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:101
|
||||
msgid "Open navigation"
|
||||
msgstr "Abrir navegação"
|
||||
|
||||
@@ -4940,12 +4959,12 @@ msgid "Password updated!"
|
||||
msgstr "Senha atualizada!"
|
||||
|
||||
#: src/view/com/util/post-embeds/GifEmbed.tsx:46
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:366
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:141
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:369
|
||||
msgid "Pause"
|
||||
msgstr "Pausar"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:319
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:320
|
||||
msgid "Pause video"
|
||||
msgstr "Pausar vídeo"
|
||||
|
||||
@@ -5005,8 +5024,8 @@ msgid "Pinned to your feeds"
|
||||
msgstr "Fixado em seus feeds"
|
||||
|
||||
#: src/view/com/util/post-embeds/GifEmbed.tsx:46
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:150
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:367
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:141
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:370
|
||||
msgid "Play"
|
||||
msgstr "Tocar"
|
||||
|
||||
@@ -5024,7 +5043,7 @@ msgid "Play or pause the GIF"
|
||||
msgstr "Tocar ou pausar o GIF"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbed.tsx:110
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:320
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:321
|
||||
msgid "Play video"
|
||||
msgstr "Reproduzir vídeo"
|
||||
|
||||
@@ -5447,7 +5466,7 @@ msgstr "Remover conta"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:28
|
||||
msgid "Remove attachment"
|
||||
msgstr ""
|
||||
msgstr "Remover anexo"
|
||||
|
||||
#: src/view/com/util/UserAvatar.tsx:393
|
||||
msgid "Remove Avatar"
|
||||
@@ -5567,7 +5586,7 @@ msgstr "Remove o post citado"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
|
||||
msgid "Removes the attachment"
|
||||
msgstr ""
|
||||
msgstr "Remove o anexo"
|
||||
|
||||
#: src/view/com/composer/ExternalEmbedRemoveBtn.tsx:29
|
||||
#~ msgid "Removes the image preview"
|
||||
@@ -5818,11 +5837,11 @@ msgstr "Reenviar e-mail"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "Reenviar E-mail"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "Reenviar E-mail de Verificação"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -6092,7 +6111,7 @@ msgstr "Veja o guia"
|
||||
#~ msgid "See what's next"
|
||||
#~ msgstr "Veja o que vem por aí"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:631
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/Scrubber.tsx:189
|
||||
msgid "Seek slider"
|
||||
msgstr "Controle deslizante de busca"
|
||||
|
||||
@@ -6152,7 +6171,7 @@ msgstr "Seleciona opção {i} de {numItems}"
|
||||
#~ msgid "Select some accounts below to follow"
|
||||
#~ msgstr "Selecione algumas contas para seguir"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:65
|
||||
#: src/view/com/composer/videos/SubtitleFilePicker.tsx:66
|
||||
msgid "Select subtitle file (.vtt)"
|
||||
msgstr "Selecione o arquivo de legenda (.vtt)"
|
||||
|
||||
@@ -6360,7 +6379,7 @@ msgstr "Atividade sexual ou nudez erótica."
|
||||
msgid "Sexually Suggestive"
|
||||
msgstr "Sexualmente Sugestivo"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:644
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:708
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:177
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:411
|
||||
#: src/screens/StarterPack/StarterPackScreen.tsx:582
|
||||
@@ -6397,11 +6416,11 @@ msgstr "Compartilhar assim"
|
||||
msgid "Share feed"
|
||||
msgstr "Compartilhar feed"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:685
|
||||
msgid "Share image externally"
|
||||
msgstr "Compartilhar imagem externamente"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:703
|
||||
msgid "Share image in post"
|
||||
msgstr "Compartilhe a imagem na postagem"
|
||||
|
||||
@@ -6692,10 +6711,6 @@ msgstr "Alguns outros feeds que você pode gostar"
|
||||
msgid "Some people can reply"
|
||||
msgstr "Algumas pessoas podem responder"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:203
|
||||
#~ msgid "Some subtitle"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/screens/Messages/Conversation/index.tsx:106
|
||||
msgid "Something went wrong"
|
||||
msgstr "Algo deu errado"
|
||||
@@ -6729,14 +6744,6 @@ msgstr "Classificar Respostas"
|
||||
msgid "Sort replies to the same post by:"
|
||||
msgstr "Classificar respostas de um post por:"
|
||||
|
||||
#: src/components/moderation/LabelsOnMeDialog.tsx:168
|
||||
#~ msgid "Source:"
|
||||
#~ msgstr "Fonte:"
|
||||
|
||||
#: src/components/moderation/LabelsOnMeDialog.tsx:169
|
||||
#~ msgid "Source: <0>{0}</0>"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/components/moderation/LabelsOnMeDialog.tsx:163
|
||||
msgid "Source: <0>{sourceName}</0>"
|
||||
msgstr "Fonte: <0>{sourceName}</0>"
|
||||
@@ -6922,22 +6929,22 @@ msgstr "Alto"
|
||||
msgid "Tap to dismiss"
|
||||
msgstr "Toque para dispensar"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:145
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:136
|
||||
msgid "Tap to enter full screen"
|
||||
msgstr "Toque para entrar em tela cheia"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:142
|
||||
msgid "Tap to play or pause"
|
||||
msgstr ""
|
||||
msgstr "Toque para reproduzir ou pausar"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:159
|
||||
msgid "Tap to toggle sound"
|
||||
msgstr "Toque para alternar o som"
|
||||
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:219
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:239
|
||||
msgid "Tap to view full image"
|
||||
msgstr ""
|
||||
msgstr "Toque para ver a imagem completa"
|
||||
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:70
|
||||
#~ msgid "Tap to view fully"
|
||||
@@ -6964,7 +6971,7 @@ msgstr "Conte uma piada!"
|
||||
msgid "Tell us a little more"
|
||||
msgstr "Conte-nos um pouco mais"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:578
|
||||
msgid "Ten Million"
|
||||
msgstr "Dez milhões"
|
||||
|
||||
@@ -7005,13 +7012,13 @@ 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:593
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:654
|
||||
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 ""
|
||||
msgstr "Obrigado, seu endereço de e-mail foi verificado com sucesso."
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -7030,7 +7037,7 @@ msgstr "Este identificador de usuário já está sendo usado."
|
||||
msgid "That starter pack could not be found."
|
||||
msgstr "Esse pacote inicial não pôde ser encontrado."
|
||||
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:127
|
||||
#: src/view/com/post-thread/PostQuotes.tsx:133
|
||||
msgid "That's all, folks!"
|
||||
msgstr "É isso, pessoal!"
|
||||
|
||||
@@ -7116,7 +7123,7 @@ msgstr "Os Termos de Serviço foram movidos para"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "O código de verificação que você forneceu é inválido. Certifique-se de que você usou o link de verificação correto ou solicite novamente."
|
||||
|
||||
#: src/screens/Onboarding/StepAlgoFeeds/index.tsx:141
|
||||
#~ msgid "There are many feeds to try:"
|
||||
@@ -7485,6 +7492,10 @@ msgstr "Preferências das Threads"
|
||||
msgid "To disable the email 2FA method, please verify your access to the email address."
|
||||
msgstr "Para desabilitar o 2FA via e-mail, por favor verifique seu acesso a este endereço de e-mail."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:69
|
||||
msgid "To learn more, <0>check out our post.</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/ReportConversationPrompt.tsx:20
|
||||
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 "Para denunciar uma conversa, por favor, denuncie uma das mensagens individualmente. Isso vai permitir a análise da situação pelos nossos moderadores."
|
||||
@@ -7497,7 +7508,8 @@ 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:597
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:658
|
||||
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:63
|
||||
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."
|
||||
|
||||
@@ -7637,15 +7649,15 @@ msgid "Unlike this feed"
|
||||
msgstr "Descurtir este feed"
|
||||
|
||||
#: src/components/TagMenu/index.tsx:263
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:163
|
||||
#: src/view/screens/ProfileList.tsx:689
|
||||
msgid "Unmute"
|
||||
msgstr "Dessilenciar"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:156
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VolumeControl.tsx:93
|
||||
msgctxt "video"
|
||||
msgid "Unmute"
|
||||
msgstr ""
|
||||
msgstr "Ativar o áudio"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:115
|
||||
msgid "Unmute {truncatedTag}"
|
||||
@@ -7673,13 +7685,13 @@ msgstr "Desmutar conversa"
|
||||
msgid "Unmute thread"
|
||||
msgstr "Dessilenciar thread"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:317
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx:318
|
||||
msgid "Unmute video"
|
||||
msgstr "Desmutar vídeo"
|
||||
msgstr "Ativar o áudio do vídeo"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Unmuted"
|
||||
#~ msgstr "Desmutar"
|
||||
#~ msgstr "Áudio ativado"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:292
|
||||
#: src/view/screens/ProfileList.tsx:673
|
||||
@@ -7920,7 +7932,7 @@ msgstr "Verificar e-mail"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "Caixa de diálogo da verificação de e-mail"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -7955,8 +7967,8 @@ msgstr "Verificar Seu E-mail"
|
||||
msgid "Version {appVersion} {bundleInfo}"
|
||||
msgstr "Versão {appVersion} {bundleInfo}"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:144
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:135
|
||||
msgid "Video"
|
||||
msgstr "Vídeo"
|
||||
|
||||
@@ -7977,13 +7989,9 @@ msgstr "Vídeo não encontrado."
|
||||
msgid "Video settings"
|
||||
msgstr "Configurações de vídeo"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:93
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:84
|
||||
msgid "Video: {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/videos/state.ts:27
|
||||
#~ msgid "Videos cannot be larger than 50MB"
|
||||
#~ msgstr "Vídeos não podem ter mais de 100 MB"
|
||||
msgstr "Vídeo: {0}"
|
||||
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
|
||||
@@ -8031,6 +8039,10 @@ msgstr "Ver thread completa"
|
||||
msgid "View information about these labels"
|
||||
msgstr "Ver informações sobre estes rótulos"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/Trigger.tsx:72
|
||||
msgid "View our post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:418
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:436
|
||||
#: src/components/ProfileHoverCard/index.web.tsx:463
|
||||
@@ -8055,8 +8067,8 @@ msgstr "Ver usuários que curtiram este feed"
|
||||
msgid "View your blocked accounts"
|
||||
msgstr "Veja suas contas bloqueadas"
|
||||
|
||||
#: src/view/com/home/HomeHeaderLayout.web.tsx:79
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:86
|
||||
#: src/view/com/home/HomeHeaderLayout.web.tsx:132
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:143
|
||||
msgid "View your feeds and explore more"
|
||||
msgstr "Veja seus feeds e explore mais"
|
||||
|
||||
@@ -8101,7 +8113,7 @@ msgstr "Estimamos que sua conta estará pronta em mais ou menos {estimatedTime}.
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "Enviamos outro e-mail de verificação para <0>{0}</0>."
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -8529,7 +8541,7 @@ msgstr "Você deve estar seguindo pelo menos sete outras pessoas para gerar um p
|
||||
msgid "You must grant access to your photo library to save a QR code"
|
||||
msgstr "Você deve conceder acesso à sua biblioteca de fotos para salvar o QR code."
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:237
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:302
|
||||
#: src/components/StarterPack/ShareDialog.tsx:68
|
||||
msgid "You must grant access to your photo library to save the image."
|
||||
msgstr "Você deve conceder acesso à sua biblioteca de fotos para salvar a imagem."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: zh_CN\n"
|
||||
"Project-Id-Version: zh-CN for bluesky-social-app\n"
|
||||
"Report-Msgid-Bugs-To: Frudrax Cheng <i@cynosura.one>\n"
|
||||
"PO-Revision-Date: 2024-09-07 22:51+0800\n"
|
||||
"PO-Revision-Date: 2024-09-14 10:09+0800\n"
|
||||
"Last-Translator: Frudrax Cheng <i@cynosura.one>\n"
|
||||
"Language-Team: Frudrax Cheng (auroursa), Simon Chan (RitsukiP), U2FsdGVkX1, Mikan Harada (mitian233), IceCodeNew\n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -544,7 +544,7 @@ msgstr "播放视频时出现问题,请再试一次。"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:250
|
||||
msgid "An error occurred while saving the image!"
|
||||
msgstr ""
|
||||
msgstr "保存图片时发生错误!"
|
||||
|
||||
#: src/components/StarterPack/QrCodeDialog.tsx:71
|
||||
#: src/components/StarterPack/ShareDialog.tsx:79
|
||||
@@ -850,7 +850,7 @@ msgstr "Bluesky 因朋友而更好!"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:206
|
||||
msgid "Bluesky now has over 10 million users, and I was #{0}!"
|
||||
msgstr ""
|
||||
msgstr "Bluesky 现在拥有一千万名用户,我是其中的第 #{0} 位!"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:282
|
||||
msgid "Bluesky will choose a set of recommended accounts from people in your network."
|
||||
@@ -875,7 +875,7 @@ msgstr "书籍"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:614
|
||||
msgid "Brag a little!"
|
||||
msgstr ""
|
||||
msgstr "吹个牛!"
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:350
|
||||
msgid "Browse more accounts on the Explore page"
|
||||
@@ -1018,7 +1018,7 @@ msgstr "字幕及替代文本"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:368
|
||||
msgid "Celebrating {0} users"
|
||||
msgstr ""
|
||||
msgstr "庆祝第 {0} 位用户"
|
||||
|
||||
#: src/view/com/modals/VerifyEmail.tsx:160
|
||||
msgid "Change"
|
||||
@@ -1099,14 +1099,6 @@ msgstr "在这里输入刚才发送到你电子邮箱里的验证码。"
|
||||
msgid "Check your inbox for an email with the confirmation code to enter below:"
|
||||
msgstr "查看发送至你电子邮箱的确认邮件,并在下方输入收到的验证码:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:191
|
||||
#~ msgid "Choose 3 or more:"
|
||||
#~ msgstr "选择至少 3 个或更多:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:326
|
||||
#~ msgid "Choose at least {0} more"
|
||||
#~ msgstr "还需选择至少 {0} 个"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:190
|
||||
msgid "Choose Feeds"
|
||||
msgstr "选择资讯源"
|
||||
@@ -1769,7 +1761,7 @@ msgstr "隆重介绍私信功能!"
|
||||
|
||||
#: src/view/screens/AccessibilitySettings.tsx:111
|
||||
msgid "Disable autoplay for videos and GIFs"
|
||||
msgstr "关闭自动播放 GIF 及视频"
|
||||
msgstr "关闭自动播放视频及 GIF"
|
||||
|
||||
#: src/view/screens/Settings/DisableEmail2FADialog.tsx:90
|
||||
msgid "Disable Email 2FA"
|
||||
@@ -1818,10 +1810,6 @@ msgstr "探索新的资讯源"
|
||||
msgid "Discover New Feeds"
|
||||
msgstr "探索新的资讯源"
|
||||
|
||||
#: src/view/com/profile/ProfileHeaderSuggestedFollows.tsx:108
|
||||
#~ msgid "Dismiss"
|
||||
#~ msgstr "关闭"
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1107
|
||||
msgid "Dismiss error"
|
||||
msgstr "关闭错误"
|
||||
@@ -1909,7 +1897,7 @@ msgstr "下载 CAR 文件"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:622
|
||||
msgid "Download image"
|
||||
msgstr ""
|
||||
msgstr "下载图片"
|
||||
|
||||
#: src/view/com/composer/text-input/TextInput.web.tsx:269
|
||||
msgid "Drop to add images"
|
||||
@@ -2073,7 +2061,7 @@ msgstr "邮箱地址"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "重新发送电子邮件"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2090,7 +2078,7 @@ msgstr "电子邮箱已验证"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "电子邮箱已验证"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2554,16 +2542,16 @@ msgstr "关注账户"
|
||||
msgid "Follow all"
|
||||
msgstr "关注所有人"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
msgid "Follow Back"
|
||||
msgstr "回关"
|
||||
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr "回关"
|
||||
|
||||
#: src/view/screens/Search/Explore.tsx:334
|
||||
msgid "Follow more accounts to get connected to your interests and build your network."
|
||||
msgstr "关注更多账户以了解你的兴趣,并逐步建立你的社交网络。"
|
||||
@@ -2922,7 +2910,7 @@ msgstr "无法加载此内容审核提供服务。"
|
||||
|
||||
#: src/state/queries/video/video.ts:165
|
||||
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
|
||||
msgstr ""
|
||||
msgstr "请稍等!我们正在逐步开放视频功能权限,你仍在等待队伍中。请稍后再回来查看!"
|
||||
|
||||
#: src/Navigation.tsx:550
|
||||
#: src/Navigation.tsx:570
|
||||
@@ -3088,7 +3076,7 @@ msgstr "用户名或密码无效"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "验证码无效"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:94
|
||||
msgid "Invite a Friend"
|
||||
@@ -3147,7 +3135,7 @@ msgstr "加入对话"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:492
|
||||
msgid "Joined {0}"
|
||||
msgstr ""
|
||||
msgstr "{0} 已加入"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:21
|
||||
#: src/screens/Onboarding/state.ts:89
|
||||
@@ -3612,16 +3600,16 @@ 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/VideoEmbedInnerNative.tsx:163
|
||||
msgid "Mute"
|
||||
msgstr "隐藏"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:389
|
||||
msgctxt "video"
|
||||
msgid "Mute"
|
||||
msgstr "隐藏"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:116
|
||||
msgid "Mute {truncatedTag}"
|
||||
msgstr "隐藏 {truncatedTag}"
|
||||
@@ -3690,10 +3678,6 @@ msgstr "隐藏讨论串"
|
||||
msgid "Mute words & tags"
|
||||
msgstr "隐藏词和标签"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Muted"
|
||||
#~ msgstr "已隐藏"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:264
|
||||
msgid "Muted accounts"
|
||||
msgstr "已隐藏账户"
|
||||
@@ -4081,7 +4065,7 @@ msgstr "糟糕!发生了一些错误。"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:175
|
||||
msgid "Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋"
|
||||
msgstr ""
|
||||
msgstr "糟糕!我们无法生成图片供你分享。请放心,我们很高兴你还在这里 🦋"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:339
|
||||
msgid "OK"
|
||||
@@ -4669,7 +4653,7 @@ msgstr "点按重试"
|
||||
|
||||
#: src/components/KnownFollowers.tsx:124
|
||||
msgid "Press to view followers of this account that you also follow"
|
||||
msgstr "点按以查看同样关注此账号的共同关注者"
|
||||
msgstr "点按以查看同样关注此账户的共同关注者"
|
||||
|
||||
#: src/view/com/lightbox/Lightbox.web.tsx:150
|
||||
msgid "Previous image"
|
||||
@@ -5205,11 +5189,11 @@ msgstr "重新发送电子邮件"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "重新发送电子邮件"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "重新发送验证码电子邮件"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -5725,11 +5709,11 @@ msgstr "分享资讯源"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:621
|
||||
msgid "Share image externally"
|
||||
msgstr ""
|
||||
msgstr "向外部分享图片"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:639
|
||||
msgid "Share image in post"
|
||||
msgstr ""
|
||||
msgstr "在帖文里分享图片"
|
||||
|
||||
#: src/components/StarterPack/ShareDialog.tsx:124
|
||||
#: src/components/StarterPack/ShareDialog.tsx:131
|
||||
@@ -5797,10 +5781,6 @@ msgstr "显示徽章"
|
||||
msgid "Show badge and filter from feeds"
|
||||
msgstr "显示徽章并从资讯源中过滤"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
|
||||
#~ msgid "Show follows similar to {0}"
|
||||
#~ msgstr "显示类似于 {0} 的关注者"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
|
||||
msgid "Show hidden replies"
|
||||
msgstr "显示已隐藏的回复"
|
||||
@@ -6117,7 +6097,7 @@ msgstr "订阅这个列表"
|
||||
|
||||
#: src/view/screens/Search/Explore.tsx:332
|
||||
msgid "Suggested accounts"
|
||||
msgstr "建议的账号"
|
||||
msgstr "建议的账户"
|
||||
|
||||
#: src/components/FeedInterstitials.tsx:318
|
||||
msgid "Suggested for you"
|
||||
@@ -6177,7 +6157,7 @@ msgstr "点击进入全屏模式"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:151
|
||||
msgid "Tap to play or pause"
|
||||
msgstr ""
|
||||
msgstr "点击以播放或静音"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:164
|
||||
msgid "Tap to toggle sound"
|
||||
@@ -6211,7 +6191,7 @@ msgstr "告诉我们更多"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:518
|
||||
msgid "Ten Million"
|
||||
msgstr ""
|
||||
msgstr "一千万"
|
||||
|
||||
#: src/view/shell/desktop/RightNav.tsx:90
|
||||
msgid "Terms"
|
||||
@@ -6248,11 +6228,11 @@ msgstr "谢谢,你的举报已提交。"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:593
|
||||
msgid "Thanks for being one of our first 10 million users."
|
||||
msgstr ""
|
||||
msgstr "感谢你成为我们的首批一千万用户的其中一份子。"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
|
||||
msgid "Thanks, you have successfully verified your email address."
|
||||
msgstr ""
|
||||
msgstr "谢谢,你已成功验证邮箱地址。"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -6353,7 +6333,7 @@ msgstr "服务条款已迁移至"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "你提供的验证码无效,请检查你所使用的验证链接,或是申请新的验证链接。"
|
||||
|
||||
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
|
||||
msgid "There is no time limit for account deactivation, come back any time."
|
||||
@@ -6501,7 +6481,7 @@ msgstr "没有 Bluesky 账户,无法查看此内容。"
|
||||
|
||||
#: src/screens/Messages/List/ChatListItem.tsx:213
|
||||
msgid "This conversation is with a deleted or a deactivated account. Press for options."
|
||||
msgstr "此对话的参与者已停用或删除账号,点击以获取更多详情。"
|
||||
msgstr "此对话的参与者已停用或删除账户,点击以获取更多详情。"
|
||||
|
||||
#: src/view/screens/Settings/ExportCarDialog.tsx:93
|
||||
msgid "This feature is in beta. You can read more about repository exports in <0>this blogpost</0>."
|
||||
@@ -6682,7 +6662,7 @@ msgstr "你想将举报提交给谁?"
|
||||
|
||||
#: src/components/dialogs/nuxs/TenMillion/index.tsx:597
|
||||
msgid "Together, we're rebuilding the social internet. We're glad you're here."
|
||||
msgstr ""
|
||||
msgstr "很高兴你能来到这里,让我们一起重新打造社交网络。"
|
||||
|
||||
#: src/view/com/util/forms/DropdownButton.tsx:255
|
||||
msgid "Toggle dropdown"
|
||||
@@ -6807,17 +6787,17 @@ 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/VideoEmbedInnerNative.tsx:163
|
||||
#: src/view/screens/ProfileList.tsx:689
|
||||
msgid "Unmute"
|
||||
msgstr "取消隐藏"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
|
||||
msgctxt "video"
|
||||
msgid "Unmute"
|
||||
msgstr "取消隐藏"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:115
|
||||
msgid "Unmute {truncatedTag}"
|
||||
msgstr "取消隐藏 {truncatedTag}"
|
||||
@@ -6844,10 +6824,6 @@ msgstr "取消隐藏讨论串"
|
||||
msgid "Unmute video"
|
||||
msgstr "取消隐藏视频"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx:168
|
||||
#~ msgid "Unmuted"
|
||||
#~ msgstr "已取消隐藏"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:292
|
||||
#: src/view/screens/ProfileList.tsx:673
|
||||
msgid "Unpin"
|
||||
@@ -7075,7 +7051,7 @@ msgstr "验证邮箱"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "验证邮箱对话框"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -7135,7 +7111,7 @@ msgstr "视频:{0}"
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
|
||||
msgid "Videos must be less than 60 seconds long"
|
||||
msgstr ""
|
||||
msgstr "视频必须短于60秒"
|
||||
|
||||
#: src/screens/Profile/Header/Shell.tsx:113
|
||||
msgid "View {0}'s avatar"
|
||||
@@ -7200,7 +7176,7 @@ msgstr "查看这个资讯源被谁喜欢"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:274
|
||||
msgid "View your blocked accounts"
|
||||
msgstr "查看你屏蔽的账号"
|
||||
msgstr "查看你屏蔽的账户"
|
||||
|
||||
#: src/view/com/home/HomeHeaderLayout.web.tsx:79
|
||||
#: src/view/com/home/HomeHeaderLayoutMobile.tsx:86
|
||||
@@ -7213,7 +7189,7 @@ msgstr "查看你的内容审核列表"
|
||||
|
||||
#: src/screens/Moderation/index.tsx:259
|
||||
msgid "View your muted accounts"
|
||||
msgstr "查看你隐藏的账号"
|
||||
msgstr "查看你隐藏的账户"
|
||||
|
||||
#: src/view/com/modals/LinkWarning.tsx:89
|
||||
#: src/view/com/modals/LinkWarning.tsx:95
|
||||
@@ -7248,7 +7224,7 @@ msgstr "我们估计还需要 {estimatedTime} 才能完成你的账户准备。"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "我们将发送另一封验证邮件至 <0>{0}</0>。"
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -7260,7 +7236,7 @@ msgstr "我们已经看完了你关注的帖文。这是来自 <0/> 的最新消
|
||||
|
||||
#: src/state/queries/video/video.ts:170
|
||||
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
|
||||
msgstr ""
|
||||
msgstr "我们无法确定你是否有权上传视频,请重试。"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:52
|
||||
msgid "We were unable to load your birth date preferences. Please try again."
|
||||
@@ -7464,7 +7440,7 @@ msgstr "轮到你了。"
|
||||
|
||||
#: src/state/queries/video/video.ts:161
|
||||
msgid "You are not allowed to upload videos."
|
||||
msgstr ""
|
||||
msgstr "你无法上传视频。"
|
||||
|
||||
#: src/view/com/profile/ProfileFollows.tsx:95
|
||||
msgid "You are not following anyone."
|
||||
@@ -7585,7 +7561,7 @@ msgstr "你已经到末尾了"
|
||||
|
||||
#: src/state/queries/video/video-upload.shared.ts:67
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
msgstr "您已暂时达到视频上传的限制,请稍后重试。"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
@@ -7711,11 +7687,11 @@ msgstr "你已经浏览完你的订阅资讯源啦!寻找一些更多的账户
|
||||
|
||||
#: src/state/queries/video/video.ts:175
|
||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||
msgstr ""
|
||||
msgstr "你已达到每日上传视频上限(文件太大)"
|
||||
|
||||
#: src/state/queries/video/video.ts:180
|
||||
msgid "You've reached your daily limit for video uploads (too many videos)"
|
||||
msgstr ""
|
||||
msgstr "你已达到每日上传视频上限(数量太多)"
|
||||
|
||||
#: src/screens/Signup/index.tsx:146
|
||||
msgid "Your account"
|
||||
@@ -7727,7 +7703,7 @@ msgstr "你的账户已删除"
|
||||
|
||||
#: src/state/queries/video/video.ts:185
|
||||
msgid "Your account is not yet old enough to upload videos. Please try again later."
|
||||
msgstr ""
|
||||
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."
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: zh-TW for bluesky-social-app\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"Report-Msgid-Bugs-To: Kuwa Lee <kuwalee1069@gmail.com>, Frudrax Cheng <i@cynosura.one>\n"
|
||||
"PO-Revision-Date: 2024-09-06 10:08+0800\n"
|
||||
"PO-Revision-Date: 2024-09-13 09:16+0800\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Frudrax Cheng <i@cynosura.one>, Kuwa Lee <kuwalee1069@gmail.com>, noeFly, snowleo208, Kisaragi Hiu, Yi-Jyun Pan, toto6038, cirx1e\n"
|
||||
"Language: zh_TW\n"
|
||||
@@ -1099,14 +1099,6 @@ msgstr "在此輸入寄送至您電子郵件地址的驗證碼。"
|
||||
msgid "Check your inbox for an email with the confirmation code to enter below:"
|
||||
msgstr "在下方輸入寄送至您電子郵件地址的驗證碼:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:191
|
||||
#~ msgid "Choose 3 or more:"
|
||||
#~ msgstr "選擇至少 3 個:"
|
||||
|
||||
#: src/screens/Onboarding/StepInterests/index.tsx:326
|
||||
#~ msgid "Choose at least {0} more"
|
||||
#~ msgstr "選擇至少 {0} 個"
|
||||
|
||||
#: src/screens/StarterPack/Wizard/index.tsx:190
|
||||
msgid "Choose Feeds"
|
||||
msgstr "選擇動態源"
|
||||
@@ -2073,7 +2065,7 @@ msgstr "電子郵件地址"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:95
|
||||
msgid "Email Resent"
|
||||
msgstr ""
|
||||
msgstr "重新發送電子郵件"
|
||||
|
||||
#: src/view/com/modals/ChangeEmail.tsx:54
|
||||
#: src/view/com/modals/ChangeEmail.tsx:83
|
||||
@@ -2090,7 +2082,7 @@ msgstr "電子郵件已驗證"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:71
|
||||
msgid "Email Verified"
|
||||
msgstr ""
|
||||
msgstr "電子郵件地址已驗證"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:319
|
||||
msgid "Email:"
|
||||
@@ -2152,7 +2144,7 @@ msgstr "已經到底部啦!"
|
||||
|
||||
#: src/view/com/composer/videos/SubtitleDialog.tsx:161
|
||||
msgid "Ensure you have selected a language for each subtitle file."
|
||||
msgstr ""
|
||||
msgstr "請確認您已為每個字幕檔案選擇一種語言。"
|
||||
|
||||
#: src/view/com/modals/AddAppPasswords.tsx:161
|
||||
msgid "Enter a name for this App Password"
|
||||
@@ -2557,7 +2549,7 @@ msgstr "全部跟隨"
|
||||
#: src/view/com/profile/FollowButton.tsx:79
|
||||
msgctxt "action"
|
||||
msgid "Follow Back"
|
||||
msgstr ""
|
||||
msgstr "回跟"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:223
|
||||
#: src/view/com/post-thread/PostThreadFollowBtn.tsx:142
|
||||
@@ -2922,7 +2914,7 @@ msgstr "抱歉,我們無法載入該內容管理服務。"
|
||||
|
||||
#: src/state/queries/video/video.ts:165
|
||||
msgid "Hold up! We’re gradually giving access to video, and you’re still waiting in line. Check back soon!"
|
||||
msgstr ""
|
||||
msgstr "別急!我們將逐步開放影片功能,您還在隊列中。請稍後再回來!"
|
||||
|
||||
#: src/Navigation.tsx:550
|
||||
#: src/Navigation.tsx:570
|
||||
@@ -3088,7 +3080,7 @@ msgstr "用戶名稱或密碼無效"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:82
|
||||
msgid "Invalid Verification Code"
|
||||
msgstr ""
|
||||
msgstr "無效的驗證碼"
|
||||
|
||||
#: src/view/com/modals/InviteCodes.tsx:94
|
||||
msgid "Invite a Friend"
|
||||
@@ -3128,7 +3120,7 @@ msgstr "現在只有您一個人!使用上面的搜尋功能,將更多人加
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1126
|
||||
msgid "Job ID: {0}"
|
||||
msgstr ""
|
||||
msgstr "Job ID: {0}"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:164
|
||||
msgid "Jobs"
|
||||
@@ -5205,11 +5197,11 @@ msgstr "重新傳送郵件"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:130
|
||||
msgid "Resend Email"
|
||||
msgstr ""
|
||||
msgstr "重新傳送電子郵件"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:123
|
||||
msgid "Resend Verification Email"
|
||||
msgstr ""
|
||||
msgstr "重新傳送驗證電子郵件"
|
||||
|
||||
#: src/view/com/modals/ChangePassword.tsx:186
|
||||
msgid "Reset code"
|
||||
@@ -5797,10 +5789,6 @@ msgstr "顯示標記"
|
||||
msgid "Show badge and filter from feeds"
|
||||
msgstr "顯示標記並從動態源中篩選"
|
||||
|
||||
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:218
|
||||
#~ msgid "Show follows similar to {0}"
|
||||
#~ msgstr "顯示類似於 {0} 的跟隨者"
|
||||
|
||||
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
|
||||
msgid "Show hidden replies"
|
||||
msgstr "顯示隱藏回覆"
|
||||
@@ -6252,7 +6240,7 @@ msgstr ""
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:74
|
||||
msgid "Thanks, you have successfully verified your email address."
|
||||
msgstr ""
|
||||
msgstr "謝謝,您已成功驗證您的電子郵件地址。"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:459
|
||||
msgid "That contains the following:"
|
||||
@@ -6353,7 +6341,7 @@ msgstr "服務條款已遷移到"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:85
|
||||
msgid "The verification code you have provided is invalid. Please make sure that you have used the correct verification link or request a new one."
|
||||
msgstr ""
|
||||
msgstr "您提供的驗證碼無效。請確認您使用了正確的驗證連結,或申請新的驗證連結。"
|
||||
|
||||
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
|
||||
msgid "There is no time limit for account deactivation, come back any time."
|
||||
@@ -6818,6 +6806,11 @@ msgstr ""
|
||||
msgid "Unmute"
|
||||
msgstr "取消靜音"
|
||||
|
||||
#: src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx:388
|
||||
msgctxt "video"
|
||||
msgid "Unmute"
|
||||
msgstr "取消靜音"
|
||||
|
||||
#: src/components/TagMenu/index.web.tsx:115
|
||||
msgid "Unmute {truncatedTag}"
|
||||
msgstr "取消靜音 {truncatedTag}"
|
||||
@@ -7075,7 +7068,7 @@ msgstr "驗證電子郵件"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:61
|
||||
msgid "Verify email dialog"
|
||||
msgstr ""
|
||||
msgstr "驗證電子郵件對話窗"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:961
|
||||
msgid "Verify my email"
|
||||
@@ -7135,7 +7128,7 @@ msgstr "影片:{0}"
|
||||
#: src/view/com/composer/videos/SelectVideoBtn.tsx:65
|
||||
#: src/view/com/composer/videos/VideoPreview.web.tsx:44
|
||||
msgid "Videos must be less than 60 seconds long"
|
||||
msgstr ""
|
||||
msgstr "影片長度必須少於 60 秒"
|
||||
|
||||
#: src/screens/Profile/Header/Shell.tsx:113
|
||||
msgid "View {0}'s avatar"
|
||||
@@ -7248,7 +7241,7 @@ msgstr "我們估計還需要 {estimatedTime} 才能準備好您的帳號。"
|
||||
|
||||
#: src/components/intents/VerifyEmailIntentDialog.tsx:98
|
||||
msgid "We have sent another verification email to <0>{0}</0>."
|
||||
msgstr ""
|
||||
msgstr "我們已發送另一封驗證電子郵件至 <0>{0}</0>。"
|
||||
|
||||
#: src/screens/Onboarding/StepFinished.tsx:238
|
||||
msgid "We hope you have a wonderful time. Remember, Bluesky is:"
|
||||
@@ -7260,7 +7253,7 @@ msgstr "您已看完了您跟隨的貼文。這是來自 <0/> 的最新貼文。
|
||||
|
||||
#: src/state/queries/video/video.ts:170
|
||||
msgid "We were unable to determine if you are allowed to upload videos. Please try again."
|
||||
msgstr ""
|
||||
msgstr "我們無法確定您是否有上傳影片的權限。請再試一次。"
|
||||
|
||||
#: src/components/dialogs/BirthDateSettings.tsx:52
|
||||
msgid "We were unable to load your birth date preferences. Please try again."
|
||||
@@ -7464,7 +7457,7 @@ msgstr "您正處於隊列之中。"
|
||||
|
||||
#: src/state/queries/video/video.ts:161
|
||||
msgid "You are not allowed to upload videos."
|
||||
msgstr ""
|
||||
msgstr "您沒有上傳影片的權限。"
|
||||
|
||||
#: src/view/com/profile/ProfileFollows.tsx:95
|
||||
msgid "You are not following anyone."
|
||||
@@ -7585,7 +7578,7 @@ msgstr "已經到底部啦!"
|
||||
|
||||
#: src/state/queries/video/video-upload.shared.ts:67
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
msgstr "您已暫時達到影片上傳的限制。請稍後再試。"
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:235
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
@@ -7711,11 +7704,11 @@ msgstr "您已經瀏覽完貼文啦!跟隨其他帳號吧。"
|
||||
|
||||
#: src/state/queries/video/video.ts:175
|
||||
msgid "You've reached your daily limit for video uploads (too many bytes)"
|
||||
msgstr ""
|
||||
msgstr "您已達到每日影片上傳限制(位元組過多)"
|
||||
|
||||
#: src/state/queries/video/video.ts:180
|
||||
msgid "You've reached your daily limit for video uploads (too many videos)"
|
||||
msgstr ""
|
||||
msgstr "您已達到每日影片上傳限制(影片數過多)"
|
||||
|
||||
#: src/screens/Signup/index.tsx:146
|
||||
msgid "Your account"
|
||||
@@ -7727,7 +7720,7 @@ msgstr "您的帳號已刪除"
|
||||
|
||||
#: src/state/queries/video/video.ts:185
|
||||
msgid "Your account is not yet old enough to upload videos. Please try again later."
|
||||
msgstr ""
|
||||
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."
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {ImageModel} from './image'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {openPicker} from 'lib/media/picker'
|
||||
|
||||
import {getImageDim} from 'lib/media/manip'
|
||||
import {openPicker} from 'lib/media/picker'
|
||||
import {ImageInitOptions, ImageModel} from './image'
|
||||
|
||||
interface InitialImageUri {
|
||||
uri: string
|
||||
width: number
|
||||
height: number
|
||||
altText?: string
|
||||
}
|
||||
|
||||
export class GalleryModel {
|
||||
images: ImageModel[] = []
|
||||
|
||||
constructor(uris?: {uri: string; width: number; height: number}[]) {
|
||||
constructor(uris?: InitialImageUri[]) {
|
||||
makeAutoObservable(this)
|
||||
|
||||
if (uris) {
|
||||
@@ -33,7 +34,7 @@ export class GalleryModel {
|
||||
return this.images.some(image => image.altText.trim() === '')
|
||||
}
|
||||
|
||||
*add(image_: Omit<RNImage, 'size'>) {
|
||||
*add(image_: ImageInitOptions) {
|
||||
if (this.size >= 4) {
|
||||
return
|
||||
}
|
||||
@@ -59,7 +60,6 @@ export class GalleryModel {
|
||||
path: uri,
|
||||
height,
|
||||
width,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
@@ -100,10 +100,10 @@ export class GalleryModel {
|
||||
async addFromUris(uris: InitialImageUri[]) {
|
||||
for (const uriObj of uris) {
|
||||
this.add({
|
||||
mime: 'image/jpeg',
|
||||
height: uriObj.height,
|
||||
width: uriObj.width,
|
||||
path: uriObj.uri,
|
||||
altText: uriObj.altText,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import * as ImageManipulator from 'expo-image-manipulator'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {openCropper} from 'lib/media/picker'
|
||||
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {Position} from 'react-avatar-editor'
|
||||
import {Dimensions} from 'lib/media/types'
|
||||
import {isIOS} from 'platform/detection'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import {openCropper} from 'lib/media/picker'
|
||||
import {Dimensions} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {isIOS} from 'platform/detection'
|
||||
|
||||
export interface ImageManipulationAttributes {
|
||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
||||
@@ -19,6 +20,13 @@ export interface ImageManipulationAttributes {
|
||||
flipVertical?: boolean
|
||||
}
|
||||
|
||||
export interface ImageInitOptions {
|
||||
path: string
|
||||
width: number
|
||||
height: number
|
||||
altText?: string
|
||||
}
|
||||
|
||||
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
||||
|
||||
export class ImageModel implements Omit<RNImage, 'size'> {
|
||||
@@ -41,12 +49,15 @@ export class ImageModel implements Omit<RNImage, 'size'> {
|
||||
}
|
||||
prevAttributes: ImageManipulationAttributes = {}
|
||||
|
||||
constructor(image: Omit<RNImage, 'size'>) {
|
||||
constructor(image: ImageInitOptions) {
|
||||
makeAutoObservable(this)
|
||||
|
||||
this.path = image.path
|
||||
this.width = image.width
|
||||
this.height = image.height
|
||||
if (image.altText !== undefined) {
|
||||
this.setAltText(image.altText)
|
||||
}
|
||||
}
|
||||
|
||||
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface ComposerOpts {
|
||||
mention?: string // handle of user to mention
|
||||
openEmojiPicker?: (pos: DOMRect | undefined) => void
|
||||
text?: string
|
||||
imageUris?: {uri: string; width: number; height: number}[]
|
||||
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
|
||||
}
|
||||
|
||||
type StateContext = ComposerOpts | undefined
|
||||
|
||||
@@ -28,8 +28,9 @@ export function SubtitleFilePicker({
|
||||
if (selectedFile) {
|
||||
if (
|
||||
selectedFile.type === 'text/vtt' ||
|
||||
(selectedFile.type === 'text/plain' &&
|
||||
selectedFile.name.endsWith('.vtt'))
|
||||
// HACK: sometimes the mime type is just straight-up missing
|
||||
// best we can do is check the file extension and hope for the best
|
||||
selectedFile.name.endsWith('.vtt')
|
||||
) {
|
||||
onSelectFile(selectedFile)
|
||||
} else {
|
||||
|
||||
@@ -44,7 +44,7 @@ export function VideoPreview({
|
||||
]}>
|
||||
<BlueskyVideoView
|
||||
url={video.uri}
|
||||
autoplay={autoplayDisabled}
|
||||
autoplay={!autoplayDisabled}
|
||||
beginMuted={true}
|
||||
forceTakeover={true}
|
||||
ref={playerRef}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -8,11 +17,11 @@ import {useSession} from '#/state/session'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
// import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import {Link} from '#/components/Link'
|
||||
import {useKawaiiMode} from '../../../state/preferences/kawaii'
|
||||
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
|
||||
|
||||
export function HomeHeaderLayout(props: {
|
||||
@@ -40,7 +49,42 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
const {hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
|
||||
const kawaii = useKawaiiMode()
|
||||
// TEMPORARY - REMOVE AFTER MILLY
|
||||
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
|
||||
// 🎉
|
||||
const rotate = useSharedValue(0)
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
// Run this a single time on app mount.
|
||||
React.useEffect(() => {
|
||||
if (reducedMotion) return
|
||||
|
||||
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
|
||||
rotate.value = withDelay(
|
||||
1000,
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(10, {duration: 100}),
|
||||
withSpring(0, {
|
||||
mass: 1,
|
||||
damping: 1,
|
||||
stiffness: 200,
|
||||
overshootClamping: false,
|
||||
}),
|
||||
),
|
||||
2,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}, [rotate, reducedMotion])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
rotateZ: `${rotate.value}deg`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,21 +101,30 @@ function HomeHeaderLayoutDesktopAndTablet({
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
styles.bar,
|
||||
kawaii && {paddingTop: 22, paddingBottom: 16},
|
||||
]}>
|
||||
<View
|
||||
<Animated.View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.pt_lg,
|
||||
a.m_auto,
|
||||
kawaii && {paddingTop: 4, paddingBottom: 0},
|
||||
{
|
||||
width: kawaii ? 84 : 28,
|
||||
width: 28,
|
||||
},
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Logo width={kawaii ? 60 : 28} />
|
||||
</View>
|
||||
<Trigger>
|
||||
{ctx => (
|
||||
<Icon
|
||||
width={28}
|
||||
style={{
|
||||
opacity: ctx.hovered || ctx.pressed ? 0.8 : 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Trigger>
|
||||
{/* <Logo width={28} /> */}
|
||||
</Animated.View>
|
||||
|
||||
<Link
|
||||
to="/feeds"
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useReducedMotion,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withSpring,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -11,10 +20,11 @@ import {HITSLOP_10} from 'lib/constants'
|
||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
// import {Logo} from '#/view/icons/Logo'
|
||||
import {atoms} from '#/alf'
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
|
||||
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
@@ -39,6 +49,43 @@ export function HomeHeaderLayoutMobile({
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
// TEMPORARY - REMOVE AFTER MILLY
|
||||
// This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
|
||||
// 🎉
|
||||
const rotate = useSharedValue(0)
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
// Run this a single time on app mount.
|
||||
React.useEffect(() => {
|
||||
if (reducedMotion) return
|
||||
|
||||
// Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
|
||||
rotate.value = withDelay(
|
||||
1000,
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(10, {duration: 100}),
|
||||
withSpring(0, {
|
||||
mass: 1,
|
||||
damping: 1,
|
||||
stiffness: 200,
|
||||
overshootClamping: false,
|
||||
}),
|
||||
),
|
||||
2,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}, [rotate, reducedMotion])
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
rotateZ: `${rotate.value}deg`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
@@ -59,9 +106,19 @@ export function HomeHeaderLayoutMobile({
|
||||
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View>
|
||||
<Logo width={30} />
|
||||
</View>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<Trigger>
|
||||
{ctx => (
|
||||
<Icon
|
||||
width={28}
|
||||
style={{
|
||||
opacity: ctx.pressed ? 0.8 : 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Trigger>
|
||||
{/* <Logo width={30} /> */}
|
||||
</Animated.View>
|
||||
<View
|
||||
style={[
|
||||
atoms.flex_row,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
@@ -26,6 +28,7 @@ function keyExtractor(item: GetLikes.Like) {
|
||||
}
|
||||
|
||||
export function PostLikedBy({uri}: {uri: string}) {
|
||||
const {_} = useLingui()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
@@ -78,6 +81,12 @@ export function PostLikedBy({uri}: {uri: string}) {
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoadingUri || isLoadingLikes}
|
||||
isError={isError}
|
||||
emptyType="results"
|
||||
emptyTitle={_(msg`No likes yet`)}
|
||||
emptyMessage={_(
|
||||
msg`Nobody has liked this yet. Maybe you should be the first!`,
|
||||
)}
|
||||
errorMessage={cleanError(resolveError || error)}
|
||||
sideBorders={false}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -97,11 +97,17 @@ export function PostQuotes({uri}: {uri: string}) {
|
||||
}
|
||||
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
if (isLoadingUri || isLoadingQuotes || isError) {
|
||||
if (quotes.length < 1) {
|
||||
return (
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoadingUri || isLoadingQuotes}
|
||||
isError={isError}
|
||||
emptyType="results"
|
||||
emptyTitle={_(msg`No quotes yet`)}
|
||||
emptyMessage={_(
|
||||
msg`Nobody has quoted this yet. Maybe you should be the first!`,
|
||||
)}
|
||||
errorMessage={cleanError(resolveError || error)}
|
||||
sideBorders={false}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
@@ -19,6 +21,7 @@ function keyExtractor(item: ActorDefs.ProfileViewBasic) {
|
||||
}
|
||||
|
||||
export function PostRepostedBy({uri}: {uri: string}) {
|
||||
const {_} = useLingui()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
@@ -71,6 +74,12 @@ export function PostRepostedBy({uri}: {uri: string}) {
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoadingUri || isLoadingRepostedBy}
|
||||
isError={isError}
|
||||
emptyType="results"
|
||||
emptyTitle={_(msg`No reposts yet`)}
|
||||
emptyMessage={_(
|
||||
msg`Nobody has reposted this yet. Maybe you should be the first!`,
|
||||
)}
|
||||
errorMessage={cleanError(resolveError || error)}
|
||||
sideBorders={false}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {tokens, useTheme} from '#/alf'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||
@@ -241,7 +241,13 @@ let UserAvatar = ({
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
)}
|
||||
<MediaInsetBorder style={[a.rounded_full]} />
|
||||
<MediaInsetBorder
|
||||
style={[
|
||||
{
|
||||
borderRadius: aviStyle.borderRadius,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{alert}
|
||||
</View>
|
||||
) : (
|
||||
|
||||
@@ -18,7 +18,7 @@ const Context = React.createContext<{
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
if (!isWeb) {
|
||||
throw new Error('ActiveVideoWebContext may onl be used on web.')
|
||||
throw new Error('ActiveVideoWebContext may only be used on web.')
|
||||
}
|
||||
|
||||
const [activeViewId, setActiveViewId] = useState<string | null>(null)
|
||||
|
||||
@@ -59,7 +59,7 @@ export const ExternalLinkEmbed = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden]}>
|
||||
<View style={[a.flex_col, a.rounded_sm]}>
|
||||
<LinkWrapper link={link} onOpen={onOpen} style={style}>
|
||||
{imageUri && !embedPlayerParams ? (
|
||||
<View>
|
||||
|
||||
@@ -227,8 +227,16 @@ export function ExternalPlayer({
|
||||
<Animated.View
|
||||
ref={viewRef}
|
||||
collapsable={false}
|
||||
style={[aspect, a.rounded_sm]}>
|
||||
{link.thumb && (!isPlayerActive || isLoading) && (
|
||||
style={[
|
||||
aspect,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}>
|
||||
{link.thumb && (!isPlayerActive || isLoading) ? (
|
||||
<>
|
||||
<Image
|
||||
style={[a.flex_1, styles.topRadius]}
|
||||
@@ -256,7 +264,29 @@ export function ExternalPlayer({
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Fill
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.contrast_975 : 'black',
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
opacity: 0.3,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<MediaInsetBorder
|
||||
opaque
|
||||
style={[
|
||||
{
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<PlaceholderOverlay
|
||||
isLoading={isLoading}
|
||||
isPlayerActive={isPlayerActive}
|
||||
|
||||
@@ -7,9 +7,8 @@ import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useVideoVolumeState} from 'view/com/util/post-embeds/VideoVolumeContext'
|
||||
import {useVideoMuteState} 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'
|
||||
@@ -38,7 +37,7 @@ export const VideoEmbedInnerNative = React.forwardRef(
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const {muted, setMuted} = useVideoVolumeState()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
|
||||
const [isPlaying, setIsPlaying] = React.useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = React.useState(0)
|
||||
@@ -54,16 +53,8 @@ export const VideoEmbedInnerNative = React.forwardRef(
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
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}]}>
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<BlueskyVideoView
|
||||
url={embed.playlist}
|
||||
autoplay={!autoplayDisabled && !isWithinMessage}
|
||||
@@ -128,7 +119,7 @@ function VideoControls({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const {muted} = useVideoVolumeState()
|
||||
const [muted] = useVideoMuteState()
|
||||
|
||||
// show countdown when:
|
||||
// 1. timeRemaining is a number - was seeing NaNs
|
||||
@@ -160,7 +151,11 @@ function VideoControls({
|
||||
|
||||
<ControlButton
|
||||
onPress={toggleMuted}
|
||||
label={muted ? _(msg`Unmute`) : _(msg`Mute`)}
|
||||
label={
|
||||
muted
|
||||
? _(msg({message: `Unmute`, context: 'video'}))
|
||||
: _(msg({message: `Mute`, context: 'video'}))
|
||||
}
|
||||
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||
style={{right: 6}}>
|
||||
{muted ? (
|
||||
|
||||
@@ -5,7 +5,7 @@ import Hls from 'hls.js'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {Controls} from './VideoWebControls'
|
||||
import {Controls} from './web-controls/VideoControls'
|
||||
|
||||
export function VideoEmbedInnerWeb({
|
||||
embed,
|
||||
|
||||
@@ -1,896 +0,0 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {SvgProps} from 'react-native-svg'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type Hls from 'hls.js'
|
||||
|
||||
import {isFirefox} from '#/lib/browser'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {isIPhoneWeb} from '#/platform/detection'
|
||||
import {
|
||||
useAutoplayDisabled,
|
||||
useSetSubtitlesEnabled,
|
||||
useSubtitlesEnabled,
|
||||
} from '#/state/preferences'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {
|
||||
ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon,
|
||||
ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon,
|
||||
} from '#/components/icons/ArrowsDiagonal'
|
||||
import {
|
||||
CC_Filled_Corner0_Rounded as CCActiveIcon,
|
||||
CC_Stroke2_Corner0_Rounded as CCInactiveIcon,
|
||||
} from '#/components/icons/CC'
|
||||
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 {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export function Controls({
|
||||
videoRef,
|
||||
hlsRef,
|
||||
active,
|
||||
setActive,
|
||||
focused,
|
||||
setFocused,
|
||||
onScreen,
|
||||
fullscreenRef,
|
||||
hasSubtitleTrack,
|
||||
}: {
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
hlsRef: React.RefObject<Hls | undefined>
|
||||
active: boolean
|
||||
setActive: () => void
|
||||
focused: boolean
|
||||
setFocused: (focused: boolean) => void
|
||||
onScreen: boolean
|
||||
fullscreenRef: React.RefObject<HTMLDivElement>
|
||||
hasSubtitleTrack: boolean
|
||||
}) {
|
||||
const {
|
||||
play,
|
||||
pause,
|
||||
playing,
|
||||
muted,
|
||||
toggleMute,
|
||||
togglePlayPause,
|
||||
currentTime,
|
||||
duration,
|
||||
buffering,
|
||||
error,
|
||||
canPlay,
|
||||
} = useVideoUtils(videoRef)
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const subtitlesEnabled = useSubtitlesEnabled()
|
||||
const setSubtitlesEnabled = useSetSubtitlesEnabled()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onHover,
|
||||
onOut: onEndHover,
|
||||
} = useInteractionState()
|
||||
const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef)
|
||||
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const [interactingViaKeypress, setInteractingViaKeypress] = useState(false)
|
||||
|
||||
const onKeyDown = useCallback(() => {
|
||||
setInteractingViaKeypress(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (interactingViaKeypress) {
|
||||
document.addEventListener('click', () => setInteractingViaKeypress(false))
|
||||
return () => {
|
||||
document.removeEventListener('click', () =>
|
||||
setInteractingViaKeypress(false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [interactingViaKeypress])
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullscreen) {
|
||||
document.documentElement.style.scrollbarGutter = 'unset'
|
||||
return () => {
|
||||
document.documentElement.style.removeProperty('scrollbar-gutter')
|
||||
}
|
||||
}
|
||||
}, [isFullscreen])
|
||||
|
||||
// pause + unfocus when another video is active
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
pause()
|
||||
setFocused(false)
|
||||
}
|
||||
}, [active, pause, setFocused])
|
||||
|
||||
// autoplay/pause based on visibility
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const autoplayDisabled = useAutoplayDisabled() || isWithinMessage
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
if (onScreen) {
|
||||
if (!autoplayDisabled) play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}, [onScreen, pause, active, play, autoplayDisabled])
|
||||
|
||||
// use minimal quality when not focused
|
||||
useEffect(() => {
|
||||
if (!hlsRef.current) return
|
||||
if (focused) {
|
||||
// auto decide quality based on network conditions
|
||||
hlsRef.current.autoLevelCapping = -1
|
||||
// allow 30s of buffering
|
||||
hlsRef.current.config.maxMaxBufferLength = 30
|
||||
} else {
|
||||
// back to what we initially set
|
||||
hlsRef.current.autoLevelCapping = 0
|
||||
hlsRef.current.config.maxMaxBufferLength = 10
|
||||
}
|
||||
}, [hlsRef, focused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hlsRef.current) return
|
||||
if (hasSubtitleTrack && subtitlesEnabled && canPlay) {
|
||||
hlsRef.current.subtitleTrack = 0
|
||||
} else {
|
||||
hlsRef.current.subtitleTrack = -1
|
||||
}
|
||||
}, [hasSubtitleTrack, subtitlesEnabled, hlsRef, canPlay])
|
||||
|
||||
// clicking on any button should focus the player, if it's not already focused
|
||||
const drawFocus = useCallback(() => {
|
||||
if (!active) {
|
||||
setActive()
|
||||
}
|
||||
setFocused(true)
|
||||
}, [active, setActive, setFocused])
|
||||
|
||||
const onPressEmptySpace = useCallback(() => {
|
||||
if (!focused) {
|
||||
drawFocus()
|
||||
if (autoplayDisabled) play()
|
||||
} else {
|
||||
togglePlayPause()
|
||||
}
|
||||
}, [togglePlayPause, drawFocus, focused, autoplayDisabled, play])
|
||||
|
||||
const onPressPlayPause = useCallback(() => {
|
||||
drawFocus()
|
||||
togglePlayPause()
|
||||
}, [drawFocus, togglePlayPause])
|
||||
|
||||
const onPressSubtitles = useCallback(() => {
|
||||
drawFocus()
|
||||
setSubtitlesEnabled(!subtitlesEnabled)
|
||||
}, [drawFocus, setSubtitlesEnabled, subtitlesEnabled])
|
||||
|
||||
const onPressMute = useCallback(() => {
|
||||
drawFocus()
|
||||
toggleMute()
|
||||
}, [drawFocus, toggleMute])
|
||||
|
||||
const onPressFullscreen = useCallback(() => {
|
||||
drawFocus()
|
||||
toggleFullscreen()
|
||||
}, [drawFocus, toggleFullscreen])
|
||||
|
||||
const onSeek = useCallback(
|
||||
(time: number) => {
|
||||
if (!videoRef.current) return
|
||||
if (videoRef.current.fastSeek) {
|
||||
videoRef.current.fastSeek(time)
|
||||
} else {
|
||||
videoRef.current.currentTime = time
|
||||
}
|
||||
},
|
||||
[videoRef],
|
||||
)
|
||||
|
||||
const playStateBeforeSeekRef = useRef(false)
|
||||
|
||||
const onSeekStart = useCallback(() => {
|
||||
drawFocus()
|
||||
playStateBeforeSeekRef.current = playing
|
||||
pause()
|
||||
}, [playing, pause, drawFocus])
|
||||
|
||||
const onSeekEnd = useCallback(() => {
|
||||
if (playStateBeforeSeekRef.current) {
|
||||
play()
|
||||
}
|
||||
}, [play])
|
||||
|
||||
const seekLeft = useCallback(() => {
|
||||
if (!videoRef.current) return
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const currentTime = videoRef.current.currentTime
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const duration = videoRef.current.duration || 0
|
||||
onSeek(clamp(currentTime - 5, 0, duration))
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const seekRight = useCallback(() => {
|
||||
if (!videoRef.current) return
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const currentTime = videoRef.current.currentTime
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const duration = videoRef.current.duration || 0
|
||||
onSeek(clamp(currentTime + 5, 0, duration))
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const onPointerMoveEmptySpace = useCallback(() => {
|
||||
setShowCursor(true)
|
||||
if (cursorTimeoutRef.current) {
|
||||
clearTimeout(cursorTimeoutRef.current)
|
||||
}
|
||||
cursorTimeoutRef.current = setTimeout(() => {
|
||||
setShowCursor(false)
|
||||
onEndHover()
|
||||
}, 2000)
|
||||
}, [onEndHover])
|
||||
const onPointerLeaveEmptySpace = useCallback(() => {
|
||||
setShowCursor(false)
|
||||
if (cursorTimeoutRef.current) {
|
||||
clearTimeout(cursorTimeoutRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// these are used to trigger the hover state. on mobile, the hover state
|
||||
// should stick around for a bit after they tap, and if the controls aren't
|
||||
// present this initial tab should *only* show the controls and not activate anything
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (evt.pointerType !== 'mouse' && !hovered) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
},
|
||||
[hovered],
|
||||
)
|
||||
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const onHoverWithTimeout = useCallback(() => {
|
||||
onHover()
|
||||
clearTimeout(timeoutRef.current)
|
||||
}, [onHover])
|
||||
|
||||
const onEndHoverWithTimeout = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
// if touch, end after 3s
|
||||
// if mouse, end immediately
|
||||
if (evt.pointerType !== 'mouse') {
|
||||
setTimeout(onEndHover, 3000)
|
||||
} else {
|
||||
onEndHover()
|
||||
}
|
||||
},
|
||||
[onEndHover],
|
||||
)
|
||||
|
||||
const showControls =
|
||||
((focused || autoplayDisabled) && !playing) ||
|
||||
(interactingViaKeypress ? hasFocus : hovered)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
onClick={evt => {
|
||||
evt.stopPropagation()
|
||||
setInteractingViaKeypress(false)
|
||||
}}
|
||||
onPointerEnter={onHoverWithTimeout}
|
||||
onPointerMove={onHoverWithTimeout}
|
||||
onPointerLeave={onEndHoverWithTimeout}
|
||||
onPointerDown={onPointerDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPointerEnter={onPointerMoveEmptySpace}
|
||||
onPointerMove={onPointerMoveEmptySpace}
|
||||
onPointerLeave={onPointerLeaveEmptySpace}
|
||||
accessibilityHint={_(
|
||||
!focused
|
||||
? msg`Unmute video`
|
||||
: playing
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
)}
|
||||
style={[
|
||||
a.flex_1,
|
||||
web({cursor: showCursor || !playing ? 'pointer' : 'none'}),
|
||||
]}
|
||||
onPress={onPressEmptySpace}
|
||||
/>
|
||||
{!showControls && !focused && duration > 0 && (
|
||||
<TimeIndicator time={Math.floor(duration - currentTime)} />
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_shrink_0,
|
||||
a.w_full,
|
||||
a.px_xs,
|
||||
web({
|
||||
background:
|
||||
'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7))',
|
||||
}),
|
||||
{opacity: showControls ? 1 : 0},
|
||||
{transition: 'opacity 0.2s ease-in-out'},
|
||||
]}>
|
||||
<Scrubber
|
||||
duration={duration}
|
||||
currentTime={currentTime}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onSeekEnd={onSeekEnd}
|
||||
seekLeft={seekLeft}
|
||||
seekRight={seekRight}
|
||||
togglePlayPause={togglePlayPause}
|
||||
drawFocus={drawFocus}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.px_xs,
|
||||
a.pt_sm,
|
||||
a.pb_md,
|
||||
a.gap_md,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
]}>
|
||||
<ControlButton
|
||||
active={playing}
|
||||
activeLabel={_(msg`Pause`)}
|
||||
inactiveLabel={_(msg`Play`)}
|
||||
activeIcon={PauseIcon}
|
||||
inactiveIcon={PlayIcon}
|
||||
onPress={onPressPlayPause}
|
||||
/>
|
||||
<View style={a.flex_1} />
|
||||
<Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}>
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</Text>
|
||||
{hasSubtitleTrack && (
|
||||
<ControlButton
|
||||
active={subtitlesEnabled}
|
||||
activeLabel={_(msg`Disable subtitles`)}
|
||||
inactiveLabel={_(msg`Enable subtitles`)}
|
||||
activeIcon={CCActiveIcon}
|
||||
inactiveIcon={CCInactiveIcon}
|
||||
onPress={onPressSubtitles}
|
||||
/>
|
||||
)}
|
||||
<ControlButton
|
||||
active={muted}
|
||||
activeLabel={_(msg({message: `Unmute`, context: 'video'}))}
|
||||
inactiveLabel={_(msg({message: `Mute`, context: 'video'}))}
|
||||
activeIcon={MuteIcon}
|
||||
inactiveIcon={UnmuteIcon}
|
||||
onPress={onPressMute}
|
||||
/>
|
||||
{!isIPhoneWeb && (
|
||||
<ControlButton
|
||||
active={isFullscreen}
|
||||
activeLabel={_(msg`Exit fullscreen`)}
|
||||
inactiveLabel={_(msg`Fullscreen`)}
|
||||
activeIcon={ArrowsInIcon}
|
||||
inactiveIcon={ArrowsOutIcon}
|
||||
onPress={onPressFullscreen}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{(buffering || error) && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
{buffering && <Loader fill={t.palette.white} size="lg" />}
|
||||
{error && (
|
||||
<Text style={{color: t.palette.white}}>
|
||||
<Trans>An error occurred</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ControlButton({
|
||||
active,
|
||||
activeLabel,
|
||||
inactiveLabel,
|
||||
activeIcon: ActiveIcon,
|
||||
inactiveIcon: InactiveIcon,
|
||||
onPress,
|
||||
}: {
|
||||
active: boolean
|
||||
activeLabel: string
|
||||
inactiveLabel: string
|
||||
activeIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
|
||||
inactiveIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
|
||||
onPress: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Button
|
||||
label={active ? activeLabel : inactiveLabel}
|
||||
onPress={onPress}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
size="medium"
|
||||
style={a.p_2xs}
|
||||
hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.1)'}}>
|
||||
{active ? (
|
||||
<ActiveIcon fill={t.palette.white} width={20} />
|
||||
) : (
|
||||
<InactiveIcon fill={t.palette.white} width={20} />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function Scrubber({
|
||||
duration,
|
||||
currentTime,
|
||||
onSeek,
|
||||
onSeekEnd,
|
||||
onSeekStart,
|
||||
seekLeft,
|
||||
seekRight,
|
||||
togglePlayPause,
|
||||
drawFocus,
|
||||
}: {
|
||||
duration: number
|
||||
currentTime: number
|
||||
onSeek: (time: number) => void
|
||||
onSeekEnd: () => void
|
||||
onSeekStart: () => void
|
||||
seekLeft: () => void
|
||||
seekRight: () => void
|
||||
togglePlayPause: () => void
|
||||
drawFocus: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const [scrubberActive, setScrubberActive] = useState(false)
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onStartHover,
|
||||
onOut: onEndHover,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const [seekPosition, setSeekPosition] = useState(0)
|
||||
const isSeekingRef = useRef(false)
|
||||
const barRef = useRef<HTMLDivElement>(null)
|
||||
const circleRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const seek = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!barRef.current) return
|
||||
const {left, width} = barRef.current.getBoundingClientRect()
|
||||
const x = evt.clientX
|
||||
const percent = clamp((x - left) / width, 0, 1) * duration
|
||||
onSeek(percent)
|
||||
setSeekPosition(percent)
|
||||
},
|
||||
[duration, onSeek],
|
||||
)
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
const target = evt.target
|
||||
if (target instanceof Element) {
|
||||
evt.preventDefault()
|
||||
target.setPointerCapture(evt.pointerId)
|
||||
isSeekingRef.current = true
|
||||
seek(evt)
|
||||
setScrubberActive(true)
|
||||
onSeekStart()
|
||||
}
|
||||
},
|
||||
[seek, onSeekStart],
|
||||
)
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (isSeekingRef.current) {
|
||||
evt.preventDefault()
|
||||
seek(evt)
|
||||
}
|
||||
},
|
||||
[seek],
|
||||
)
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
const target = evt.target
|
||||
if (isSeekingRef.current && target instanceof Element) {
|
||||
evt.preventDefault()
|
||||
target.releasePointerCapture(evt.pointerId)
|
||||
isSeekingRef.current = false
|
||||
onSeekEnd()
|
||||
setScrubberActive(false)
|
||||
}
|
||||
},
|
||||
[onSeekEnd],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// HACK: there's divergent browser behaviour about what to do when
|
||||
// a pointerUp event is fired outside the element that captured the
|
||||
// pointer. Firefox clicks on the element the mouse is over, so we have
|
||||
// to make everything unclickable while seeking -sfn
|
||||
if (isFirefox && scrubberActive) {
|
||||
document.body.classList.add('force-no-clicks')
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('force-no-clicks')
|
||||
}
|
||||
}
|
||||
}, [scrubberActive, onSeekEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!circleRef.current) return
|
||||
if (focused) {
|
||||
const abortController = new AbortController()
|
||||
const {signal} = abortController
|
||||
circleRef.current.addEventListener(
|
||||
'keydown',
|
||||
evt => {
|
||||
// space: play/pause
|
||||
// arrow left: seek backward
|
||||
// arrow right: seek forward
|
||||
|
||||
if (evt.key === ' ') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
togglePlayPause()
|
||||
} else if (evt.key === 'ArrowLeft') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
seekLeft()
|
||||
} else if (evt.key === 'ArrowRight') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
seekRight()
|
||||
}
|
||||
},
|
||||
{signal},
|
||||
)
|
||||
|
||||
return () => abortController.abort()
|
||||
}
|
||||
}, [focused, seekLeft, seekRight, togglePlayPause, drawFocus])
|
||||
|
||||
const progress = scrubberActive ? seekPosition : currentTime
|
||||
const progressPercent = (progress / duration) * 100
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="scrubber"
|
||||
style={[{height: 18, width: '100%'}, a.flex_shrink_0, a.px_xs, a.py_xs]}
|
||||
onPointerEnter={onStartHover}
|
||||
onPointerLeave={onEndHover}>
|
||||
<div
|
||||
ref={barRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
cursor: scrubberActive ? 'grabbing' : 'grab',
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
{backgroundColor: 'rgba(255, 255, 255, 0.4)'},
|
||||
{height: hovered || scrubberActive ? 6 : 3},
|
||||
]}>
|
||||
{duration > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.h_full,
|
||||
{backgroundColor: t.palette.white},
|
||||
{width: `${progressPercent}%`},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<div
|
||||
ref={circleRef}
|
||||
aria-label={_(msg`Seek slider`)}
|
||||
role="slider"
|
||||
aria-valuemax={duration}
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={currentTime}
|
||||
aria-valuetext={_(
|
||||
msg`${formatTime(currentTime)} of ${formatTime(duration)}`,
|
||||
)}
|
||||
tabIndex={0}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: 16,
|
||||
width: 16,
|
||||
left: `calc(${progressPercent}% - 8px)`,
|
||||
borderRadius: 8,
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.rounded_full,
|
||||
{backgroundColor: t.palette.white},
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
scale:
|
||||
hovered || scrubberActive || focused
|
||||
? scrubberActive
|
||||
? 1
|
||||
: 0.6
|
||||
: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function formatTime(time: number) {
|
||||
if (isNaN(time)) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
time = Math.round(time)
|
||||
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = String(time % 60).padStart(2, '0')
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [buffering, setBuffering] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [canPlay, setCanPlay] = useState(false)
|
||||
const playWhenReadyRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
let bufferingTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function round(num: number) {
|
||||
return Math.round(num * 100) / 100
|
||||
}
|
||||
|
||||
// Initial values
|
||||
setCurrentTime(round(ref.current.currentTime) || 0)
|
||||
setDuration(round(ref.current.duration) || 0)
|
||||
setMuted(ref.current.muted)
|
||||
setPlaying(!ref.current.paused)
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (!ref.current) return
|
||||
setCurrentTime(round(ref.current.currentTime) || 0)
|
||||
}
|
||||
|
||||
const handleDurationChange = () => {
|
||||
if (!ref.current) return
|
||||
setDuration(round(ref.current.duration) || 0)
|
||||
}
|
||||
|
||||
const handlePlay = () => {
|
||||
setPlaying(true)
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
setPlaying(false)
|
||||
}
|
||||
|
||||
const handleVolumeChange = () => {
|
||||
if (!ref.current) return
|
||||
setMuted(ref.current.muted)
|
||||
}
|
||||
|
||||
const handleError = () => {
|
||||
setError(true)
|
||||
}
|
||||
|
||||
const handleCanPlay = () => {
|
||||
setBuffering(false)
|
||||
setCanPlay(true)
|
||||
|
||||
if (!ref.current) return
|
||||
if (playWhenReadyRef.current) {
|
||||
ref.current.play()
|
||||
playWhenReadyRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCanPlayThrough = () => {
|
||||
setBuffering(false)
|
||||
}
|
||||
|
||||
const handleWaiting = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
bufferingTimeout = setTimeout(() => {
|
||||
setBuffering(true)
|
||||
}, 200) // Delay to avoid frequent buffering state changes
|
||||
}
|
||||
|
||||
const handlePlaying = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
const handleStalled = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
bufferingTimeout = setTimeout(() => {
|
||||
setBuffering(true)
|
||||
}, 200) // Delay to avoid frequent buffering state changes
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setPlaying(false)
|
||||
setBuffering(false)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
ref.current.addEventListener('timeupdate', handleTimeUpdate, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('durationchange', handleDurationChange, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('play', handlePlay, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('pause', handlePause, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('volumechange', handleVolumeChange, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('error', handleError, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('canplay', handleCanPlay, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('canplaythrough', handleCanPlayThrough, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('waiting', handleWaiting, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('playing', handlePlaying, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('stalled', handleStalled, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('ended', handleEnded, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
clearTimeout(bufferingTimeout)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
const play = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
if (ref.current.ended) {
|
||||
ref.current.currentTime = 0
|
||||
}
|
||||
|
||||
if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
|
||||
playWhenReadyRef.current = true
|
||||
} else {
|
||||
const promise = ref.current.play()
|
||||
if (promise !== undefined) {
|
||||
promise.catch(err => {
|
||||
console.error('Error playing video:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
const pause = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
ref.current.pause()
|
||||
playWhenReadyRef.current = false
|
||||
}, [ref])
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
if (ref.current.paused) {
|
||||
play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
}, [ref, play, pause])
|
||||
|
||||
const mute = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
ref.current.muted = true
|
||||
}, [ref])
|
||||
|
||||
const unmute = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
ref.current.muted = false
|
||||
}, [ref])
|
||||
|
||||
const toggleMute = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
ref.current.muted = !ref.current.muted
|
||||
}, [ref])
|
||||
|
||||
return {
|
||||
play,
|
||||
pause,
|
||||
togglePlayPause,
|
||||
duration,
|
||||
currentTime,
|
||||
playing,
|
||||
muted,
|
||||
mute,
|
||||
unmute,
|
||||
toggleMute,
|
||||
buffering,
|
||||
error,
|
||||
canPlay,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import {SvgProps} from 'react-native-svg'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
|
||||
export function ControlButton({
|
||||
active,
|
||||
activeLabel,
|
||||
inactiveLabel,
|
||||
activeIcon: ActiveIcon,
|
||||
inactiveIcon: InactiveIcon,
|
||||
onPress,
|
||||
}: {
|
||||
active: boolean
|
||||
activeLabel: string
|
||||
inactiveLabel: string
|
||||
activeIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
|
||||
inactiveIcon: React.ComponentType<Pick<SvgProps, 'fill' | 'width'>>
|
||||
onPress: () => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Button
|
||||
label={active ? activeLabel : inactiveLabel}
|
||||
onPress={onPress}
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
size="medium"
|
||||
style={a.p_2xs}
|
||||
hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.1)'}}>
|
||||
{active ? (
|
||||
<ActiveIcon fill={t.palette.white} width={20} />
|
||||
) : (
|
||||
<InactiveIcon fill={t.palette.white} width={20} />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isFirefox} from '#/lib/browser'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {formatTime} from './utils'
|
||||
|
||||
export function Scrubber({
|
||||
duration,
|
||||
currentTime,
|
||||
onSeek,
|
||||
onSeekEnd,
|
||||
onSeekStart,
|
||||
seekLeft,
|
||||
seekRight,
|
||||
togglePlayPause,
|
||||
drawFocus,
|
||||
}: {
|
||||
duration: number
|
||||
currentTime: number
|
||||
onSeek: (time: number) => void
|
||||
onSeekEnd: () => void
|
||||
onSeekStart: () => void
|
||||
seekLeft: () => void
|
||||
seekRight: () => void
|
||||
togglePlayPause: () => void
|
||||
drawFocus: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const [scrubberActive, setScrubberActive] = useState(false)
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onStartHover,
|
||||
onOut: onEndHover,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const [seekPosition, setSeekPosition] = useState(0)
|
||||
const isSeekingRef = useRef(false)
|
||||
const barRef = useRef<HTMLDivElement>(null)
|
||||
const circleRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const seek = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!barRef.current) return
|
||||
const {left, width} = barRef.current.getBoundingClientRect()
|
||||
const x = evt.clientX
|
||||
const percent = clamp((x - left) / width, 0, 1) * duration
|
||||
onSeek(percent)
|
||||
setSeekPosition(percent)
|
||||
},
|
||||
[duration, onSeek],
|
||||
)
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
const target = evt.target
|
||||
if (target instanceof Element) {
|
||||
evt.preventDefault()
|
||||
target.setPointerCapture(evt.pointerId)
|
||||
isSeekingRef.current = true
|
||||
seek(evt)
|
||||
setScrubberActive(true)
|
||||
onSeekStart()
|
||||
}
|
||||
},
|
||||
[seek, onSeekStart],
|
||||
)
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (isSeekingRef.current) {
|
||||
evt.preventDefault()
|
||||
seek(evt)
|
||||
}
|
||||
},
|
||||
[seek],
|
||||
)
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
const target = evt.target
|
||||
if (isSeekingRef.current && target instanceof Element) {
|
||||
evt.preventDefault()
|
||||
target.releasePointerCapture(evt.pointerId)
|
||||
isSeekingRef.current = false
|
||||
onSeekEnd()
|
||||
setScrubberActive(false)
|
||||
}
|
||||
},
|
||||
[onSeekEnd],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// HACK: there's divergent browser behaviour about what to do when
|
||||
// a pointerUp event is fired outside the element that captured the
|
||||
// pointer. Firefox clicks on the element the mouse is over, so we have
|
||||
// to make everything unclickable while seeking -sfn
|
||||
if (isFirefox && scrubberActive) {
|
||||
document.body.classList.add('force-no-clicks')
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove('force-no-clicks')
|
||||
}
|
||||
}
|
||||
}, [scrubberActive, onSeekEnd])
|
||||
|
||||
useEffect(() => {
|
||||
if (!circleRef.current) return
|
||||
if (focused) {
|
||||
const abortController = new AbortController()
|
||||
const {signal} = abortController
|
||||
circleRef.current.addEventListener(
|
||||
'keydown',
|
||||
evt => {
|
||||
// space: play/pause
|
||||
// arrow left: seek backward
|
||||
// arrow right: seek forward
|
||||
|
||||
if (evt.key === ' ') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
togglePlayPause()
|
||||
} else if (evt.key === 'ArrowLeft') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
seekLeft()
|
||||
} else if (evt.key === 'ArrowRight') {
|
||||
evt.preventDefault()
|
||||
drawFocus()
|
||||
seekRight()
|
||||
}
|
||||
},
|
||||
{signal},
|
||||
)
|
||||
|
||||
return () => abortController.abort()
|
||||
}
|
||||
}, [focused, seekLeft, seekRight, togglePlayPause, drawFocus])
|
||||
|
||||
const progress = scrubberActive ? seekPosition : currentTime
|
||||
const progressPercent = (progress / duration) * 100
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="scrubber"
|
||||
style={[{height: 18, width: '100%'}, a.flex_shrink_0, a.px_xs]}
|
||||
onPointerEnter={onStartHover}
|
||||
onPointerLeave={onEndHover}>
|
||||
<div
|
||||
ref={barRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
cursor: scrubberActive ? 'grabbing' : 'grab',
|
||||
padding: '4px 0',
|
||||
}}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_full,
|
||||
a.overflow_hidden,
|
||||
{backgroundColor: 'rgba(255, 255, 255, 0.4)'},
|
||||
{height: hovered || scrubberActive ? 6 : 3},
|
||||
web({transition: 'height 0.1s ease'}),
|
||||
]}>
|
||||
{duration > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.h_full,
|
||||
{backgroundColor: t.palette.white},
|
||||
{width: `${progressPercent}%`},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<div
|
||||
ref={circleRef}
|
||||
aria-label={_(msg`Seek slider`)}
|
||||
role="slider"
|
||||
aria-valuemax={duration}
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={currentTime}
|
||||
aria-valuetext={_(
|
||||
msg`${formatTime(currentTime)} of ${formatTime(duration)}`,
|
||||
)}
|
||||
tabIndex={0}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: 16,
|
||||
width: 16,
|
||||
left: `calc(${progressPercent}% - 8px)`,
|
||||
borderRadius: 8,
|
||||
pointerEvents: 'none',
|
||||
}}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.rounded_full,
|
||||
{backgroundColor: t.palette.white},
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
scale:
|
||||
hovered || scrubberActive || focused
|
||||
? scrubberActive
|
||||
? 1
|
||||
: 0.6
|
||||
: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type Hls from 'hls.js'
|
||||
|
||||
import {isTouchDevice} from '#/lib/browser'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {isIPhoneWeb} from '#/platform/detection'
|
||||
import {
|
||||
useAutoplayDisabled,
|
||||
useSetSubtitlesEnabled,
|
||||
useSubtitlesEnabled,
|
||||
} from '#/state/preferences'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {
|
||||
ArrowsDiagonalIn_Stroke2_Corner0_Rounded as ArrowsInIcon,
|
||||
ArrowsDiagonalOut_Stroke2_Corner0_Rounded as ArrowsOutIcon,
|
||||
} from '#/components/icons/ArrowsDiagonal'
|
||||
import {
|
||||
CC_Filled_Corner0_Rounded as CCActiveIcon,
|
||||
CC_Stroke2_Corner0_Rounded as CCInactiveIcon,
|
||||
} from '#/components/icons/CC'
|
||||
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
|
||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {TimeIndicator} from '../TimeIndicator'
|
||||
import {ControlButton} from './ControlButton'
|
||||
import {Scrubber} from './Scrubber'
|
||||
import {formatTime, useVideoElement} from './utils'
|
||||
import {VolumeControl} from './VolumeControl'
|
||||
|
||||
export function Controls({
|
||||
videoRef,
|
||||
hlsRef,
|
||||
active,
|
||||
setActive,
|
||||
focused,
|
||||
setFocused,
|
||||
onScreen,
|
||||
fullscreenRef,
|
||||
hasSubtitleTrack,
|
||||
}: {
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
hlsRef: React.RefObject<Hls | undefined>
|
||||
active: boolean
|
||||
setActive: () => void
|
||||
focused: boolean
|
||||
setFocused: (focused: boolean) => void
|
||||
onScreen: boolean
|
||||
fullscreenRef: React.RefObject<HTMLDivElement>
|
||||
hasSubtitleTrack: boolean
|
||||
}) {
|
||||
const {
|
||||
play,
|
||||
pause,
|
||||
playing,
|
||||
muted,
|
||||
changeMuted,
|
||||
togglePlayPause,
|
||||
currentTime,
|
||||
duration,
|
||||
buffering,
|
||||
error,
|
||||
canPlay,
|
||||
} = useVideoElement(videoRef)
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const subtitlesEnabled = useSubtitlesEnabled()
|
||||
const setSubtitlesEnabled = useSetSubtitlesEnabled()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onHover,
|
||||
onOut: onEndHover,
|
||||
} = useInteractionState()
|
||||
const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef)
|
||||
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const [interactingViaKeypress, setInteractingViaKeypress] = useState(false)
|
||||
const {
|
||||
state: volumeHovered,
|
||||
onIn: onVolumeHover,
|
||||
onOut: onVolumeEndHover,
|
||||
} = useInteractionState()
|
||||
|
||||
const onKeyDown = useCallback(() => {
|
||||
setInteractingViaKeypress(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (interactingViaKeypress) {
|
||||
document.addEventListener('click', () => setInteractingViaKeypress(false))
|
||||
return () => {
|
||||
document.removeEventListener('click', () =>
|
||||
setInteractingViaKeypress(false),
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [interactingViaKeypress])
|
||||
|
||||
useEffect(() => {
|
||||
if (isFullscreen) {
|
||||
document.documentElement.style.scrollbarGutter = 'unset'
|
||||
return () => {
|
||||
document.documentElement.style.removeProperty('scrollbar-gutter')
|
||||
}
|
||||
}
|
||||
}, [isFullscreen])
|
||||
|
||||
// pause + unfocus when another video is active
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
pause()
|
||||
setFocused(false)
|
||||
}
|
||||
}, [active, pause, setFocused])
|
||||
|
||||
// autoplay/pause based on visibility
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const autoplayDisabled = useAutoplayDisabled() || isWithinMessage
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
if (onScreen) {
|
||||
if (!autoplayDisabled) play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
}, [onScreen, pause, active, play, autoplayDisabled])
|
||||
|
||||
// use minimal quality when not focused
|
||||
useEffect(() => {
|
||||
if (!hlsRef.current) return
|
||||
if (focused) {
|
||||
// auto decide quality based on network conditions
|
||||
hlsRef.current.autoLevelCapping = -1
|
||||
// allow 30s of buffering
|
||||
hlsRef.current.config.maxMaxBufferLength = 30
|
||||
} else {
|
||||
// back to what we initially set
|
||||
hlsRef.current.autoLevelCapping = 0
|
||||
hlsRef.current.config.maxMaxBufferLength = 10
|
||||
}
|
||||
}, [hlsRef, focused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hlsRef.current) return
|
||||
if (hasSubtitleTrack && subtitlesEnabled && canPlay) {
|
||||
hlsRef.current.subtitleTrack = 0
|
||||
} else {
|
||||
hlsRef.current.subtitleTrack = -1
|
||||
}
|
||||
}, [hasSubtitleTrack, subtitlesEnabled, hlsRef, canPlay])
|
||||
|
||||
// clicking on any button should focus the player, if it's not already focused
|
||||
const drawFocus = useCallback(() => {
|
||||
if (!active) {
|
||||
setActive()
|
||||
}
|
||||
setFocused(true)
|
||||
}, [active, setActive, setFocused])
|
||||
|
||||
const onPressEmptySpace = useCallback(() => {
|
||||
if (!focused) {
|
||||
drawFocus()
|
||||
if (autoplayDisabled) play()
|
||||
} else {
|
||||
togglePlayPause()
|
||||
}
|
||||
}, [togglePlayPause, drawFocus, focused, autoplayDisabled, play])
|
||||
|
||||
const onPressPlayPause = useCallback(() => {
|
||||
drawFocus()
|
||||
togglePlayPause()
|
||||
}, [drawFocus, togglePlayPause])
|
||||
|
||||
const onPressSubtitles = useCallback(() => {
|
||||
drawFocus()
|
||||
setSubtitlesEnabled(!subtitlesEnabled)
|
||||
}, [drawFocus, setSubtitlesEnabled, subtitlesEnabled])
|
||||
|
||||
const onPressFullscreen = useCallback(() => {
|
||||
drawFocus()
|
||||
toggleFullscreen()
|
||||
}, [drawFocus, toggleFullscreen])
|
||||
|
||||
const onSeek = useCallback(
|
||||
(time: number) => {
|
||||
if (!videoRef.current) return
|
||||
if (videoRef.current.fastSeek) {
|
||||
videoRef.current.fastSeek(time)
|
||||
} else {
|
||||
videoRef.current.currentTime = time
|
||||
}
|
||||
},
|
||||
[videoRef],
|
||||
)
|
||||
|
||||
const playStateBeforeSeekRef = useRef(false)
|
||||
|
||||
const onSeekStart = useCallback(() => {
|
||||
drawFocus()
|
||||
playStateBeforeSeekRef.current = playing
|
||||
pause()
|
||||
}, [playing, pause, drawFocus])
|
||||
|
||||
const onSeekEnd = useCallback(() => {
|
||||
if (playStateBeforeSeekRef.current) {
|
||||
play()
|
||||
}
|
||||
}, [play])
|
||||
|
||||
const seekLeft = useCallback(() => {
|
||||
if (!videoRef.current) return
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const currentTime = videoRef.current.currentTime
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const duration = videoRef.current.duration || 0
|
||||
onSeek(clamp(currentTime - 5, 0, duration))
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const seekRight = useCallback(() => {
|
||||
if (!videoRef.current) return
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const currentTime = videoRef.current.currentTime
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const duration = videoRef.current.duration || 0
|
||||
onSeek(clamp(currentTime + 5, 0, duration))
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const onPointerMoveEmptySpace = useCallback(() => {
|
||||
setShowCursor(true)
|
||||
if (cursorTimeoutRef.current) {
|
||||
clearTimeout(cursorTimeoutRef.current)
|
||||
}
|
||||
cursorTimeoutRef.current = setTimeout(() => {
|
||||
setShowCursor(false)
|
||||
onEndHover()
|
||||
}, 2000)
|
||||
}, [onEndHover])
|
||||
const onPointerLeaveEmptySpace = useCallback(() => {
|
||||
setShowCursor(false)
|
||||
if (cursorTimeoutRef.current) {
|
||||
clearTimeout(cursorTimeoutRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// these are used to trigger the hover state. on mobile, the hover state
|
||||
// should stick around for a bit after they tap, and if the controls aren't
|
||||
// present this initial tab should *only* show the controls and not activate anything
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (evt.pointerType !== 'mouse' && !hovered) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
clearTimeout(timeoutRef.current)
|
||||
},
|
||||
[hovered],
|
||||
)
|
||||
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const onHoverWithTimeout = useCallback(() => {
|
||||
onHover()
|
||||
clearTimeout(timeoutRef.current)
|
||||
}, [onHover])
|
||||
|
||||
const onEndHoverWithTimeout = useCallback(
|
||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||
// if touch, end after 3s
|
||||
// if mouse, end immediately
|
||||
if (evt.pointerType !== 'mouse') {
|
||||
setTimeout(onEndHover, 3000)
|
||||
} else {
|
||||
onEndHover()
|
||||
}
|
||||
},
|
||||
[onEndHover],
|
||||
)
|
||||
|
||||
const showControls =
|
||||
((focused || autoplayDisabled) && !playing) ||
|
||||
(interactingViaKeypress ? hasFocus : hovered)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
onClick={evt => {
|
||||
evt.stopPropagation()
|
||||
setInteractingViaKeypress(false)
|
||||
}}
|
||||
onPointerEnter={onHoverWithTimeout}
|
||||
onPointerMove={onHoverWithTimeout}
|
||||
onPointerLeave={onEndHoverWithTimeout}
|
||||
onPointerDown={onPointerDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPointerEnter={onPointerMoveEmptySpace}
|
||||
onPointerMove={onPointerMoveEmptySpace}
|
||||
onPointerLeave={onPointerLeaveEmptySpace}
|
||||
accessibilityHint={_(
|
||||
!focused
|
||||
? msg`Unmute video`
|
||||
: playing
|
||||
? msg`Pause video`
|
||||
: msg`Play video`,
|
||||
)}
|
||||
style={[
|
||||
a.flex_1,
|
||||
web({cursor: showCursor || !playing ? 'pointer' : 'none'}),
|
||||
]}
|
||||
onPress={onPressEmptySpace}
|
||||
/>
|
||||
{!showControls && !focused && duration > 0 && (
|
||||
<TimeIndicator time={Math.floor(duration - currentTime)} />
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_shrink_0,
|
||||
a.w_full,
|
||||
a.px_xs,
|
||||
web({
|
||||
background:
|
||||
'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.7))',
|
||||
}),
|
||||
{opacity: showControls ? 1 : 0},
|
||||
{transition: 'opacity 0.2s ease-in-out'},
|
||||
]}>
|
||||
{(!volumeHovered || isTouchDevice) && (
|
||||
<Scrubber
|
||||
duration={duration}
|
||||
currentTime={currentTime}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onSeekEnd={onSeekEnd}
|
||||
seekLeft={seekLeft}
|
||||
seekRight={seekRight}
|
||||
togglePlayPause={togglePlayPause}
|
||||
drawFocus={drawFocus}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.px_xs,
|
||||
a.pt_2xs,
|
||||
a.pb_md,
|
||||
a.gap_md,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
]}>
|
||||
<ControlButton
|
||||
active={playing}
|
||||
activeLabel={_(msg`Pause`)}
|
||||
inactiveLabel={_(msg`Play`)}
|
||||
activeIcon={PauseIcon}
|
||||
inactiveIcon={PlayIcon}
|
||||
onPress={onPressPlayPause}
|
||||
/>
|
||||
<View style={a.flex_1} />
|
||||
<Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}>
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</Text>
|
||||
{hasSubtitleTrack && (
|
||||
<ControlButton
|
||||
active={subtitlesEnabled}
|
||||
activeLabel={_(msg`Disable subtitles`)}
|
||||
inactiveLabel={_(msg`Enable subtitles`)}
|
||||
activeIcon={CCActiveIcon}
|
||||
inactiveIcon={CCInactiveIcon}
|
||||
onPress={onPressSubtitles}
|
||||
/>
|
||||
)}
|
||||
<VolumeControl
|
||||
muted={muted}
|
||||
changeMuted={changeMuted}
|
||||
hovered={volumeHovered}
|
||||
onHover={onVolumeHover}
|
||||
onEndHover={onVolumeEndHover}
|
||||
drawFocus={drawFocus}
|
||||
/>
|
||||
{!isIPhoneWeb && (
|
||||
<ControlButton
|
||||
active={isFullscreen}
|
||||
activeLabel={_(msg`Exit fullscreen`)}
|
||||
inactiveLabel={_(msg`Fullscreen`)}
|
||||
activeIcon={ArrowsInIcon}
|
||||
inactiveIcon={ArrowsOutIcon}
|
||||
onPress={onPressFullscreen}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{(buffering || error) && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={[a.absolute, a.inset_0, a.justify_center, a.align_center]}>
|
||||
{buffering && <Loader fill={t.palette.white} size="lg" />}
|
||||
{error && (
|
||||
<Text style={{color: t.palette.white}}>
|
||||
<Trans>An error occurred</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {ControlButton} from './ControlButton'
|
||||
|
||||
export function VolumeControl({
|
||||
muted,
|
||||
changeMuted,
|
||||
hovered,
|
||||
onHover,
|
||||
onEndHover,
|
||||
drawFocus,
|
||||
}: {
|
||||
muted: boolean
|
||||
changeMuted: (muted: boolean | ((prev: boolean) => boolean)) => void
|
||||
hovered: boolean
|
||||
onHover: () => void
|
||||
onEndHover: () => void
|
||||
drawFocus: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [volume, setVolume] = useVideoVolumeState()
|
||||
|
||||
const onVolumeChange = useCallback(
|
||||
(evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
drawFocus()
|
||||
const vol = sliderVolumeToVideoVolume(Number(evt.target.value))
|
||||
setVolume(vol)
|
||||
changeMuted(vol === 0)
|
||||
},
|
||||
[setVolume, drawFocus, changeMuted],
|
||||
)
|
||||
|
||||
const sliderVolume = muted ? 0 : videoVolumeToSliderVolume(volume)
|
||||
|
||||
const isZeroVolume = volume === 0
|
||||
const onPressMute = useCallback(() => {
|
||||
drawFocus()
|
||||
if (isZeroVolume) {
|
||||
setVolume(1)
|
||||
changeMuted(false)
|
||||
} else {
|
||||
changeMuted(prevMuted => !prevMuted)
|
||||
}
|
||||
}, [drawFocus, setVolume, isZeroVolume, changeMuted])
|
||||
|
||||
return (
|
||||
<View
|
||||
onPointerEnter={onHover}
|
||||
onPointerLeave={onEndHover}
|
||||
style={[a.relative]}>
|
||||
{hovered && !isTouchDevice && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(100)}
|
||||
exiting={FadeOut.duration(100)}
|
||||
style={[a.absolute, a.w_full, {height: 100, bottom: '100%'}]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mb_xs,
|
||||
a.px_2xs,
|
||||
a.py_xs,
|
||||
{backgroundColor: 'rgba(0, 0, 0, 0.6)'},
|
||||
a.rounded_xs,
|
||||
a.align_center,
|
||||
]}>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderVolume}
|
||||
style={
|
||||
// Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h
|
||||
isSafari ? {height: 92, minHeight: '100%'} : {height: '100%'}
|
||||
}
|
||||
onChange={onVolumeChange}
|
||||
// @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn
|
||||
orient="vertical"
|
||||
/>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
<ControlButton
|
||||
active={muted || volume === 0}
|
||||
activeLabel={_(msg({message: `Unmute`, context: 'video'}))}
|
||||
inactiveLabel={_(msg({message: `Mute`, context: 'video'}))}
|
||||
activeIcon={MuteIcon}
|
||||
inactiveIcon={UnmuteIcon}
|
||||
onPress={onPressMute}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function sliderVolumeToVideoVolume(value: number) {
|
||||
return Math.pow(value / 100, 4)
|
||||
}
|
||||
|
||||
function videoVolumeToSliderVolume(value: number) {
|
||||
return Math.round(Math.pow(value, 1 / 4) * 100)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [volume, setVolume] = useVideoVolumeState()
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [buffering, setBuffering] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [canPlay, setCanPlay] = useState(false)
|
||||
const playWhenReadyRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
ref.current.volume = volume
|
||||
}, [ref, volume])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
let bufferingTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
function round(num: number) {
|
||||
return Math.round(num * 100) / 100
|
||||
}
|
||||
|
||||
// Initial values
|
||||
setCurrentTime(round(ref.current.currentTime) || 0)
|
||||
setDuration(round(ref.current.duration) || 0)
|
||||
setMuted(ref.current.muted)
|
||||
setPlaying(!ref.current.paused)
|
||||
setVolume(ref.current.volume)
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
if (!ref.current) return
|
||||
setCurrentTime(round(ref.current.currentTime) || 0)
|
||||
// HACK: Safari randomly fires `stalled` events when changing between segments
|
||||
// let's just clear the buffering state if the video is still progressing -sfn
|
||||
if (isSafari) {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDurationChange = () => {
|
||||
if (!ref.current) return
|
||||
setDuration(round(ref.current.duration) || 0)
|
||||
}
|
||||
|
||||
const handlePlay = () => {
|
||||
setPlaying(true)
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
setPlaying(false)
|
||||
}
|
||||
|
||||
const handleVolumeChange = () => {
|
||||
if (!ref.current) return
|
||||
setMuted(ref.current.muted)
|
||||
}
|
||||
|
||||
const handleError = () => {
|
||||
setError(true)
|
||||
}
|
||||
|
||||
const handleCanPlay = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
setCanPlay(true)
|
||||
|
||||
if (!ref.current) return
|
||||
if (playWhenReadyRef.current) {
|
||||
ref.current.play()
|
||||
playWhenReadyRef.current = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCanPlayThrough = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
}
|
||||
|
||||
const handleWaiting = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
bufferingTimeout = setTimeout(() => {
|
||||
setBuffering(true)
|
||||
}, 500) // Delay to avoid frequent buffering state changes
|
||||
}
|
||||
|
||||
const handlePlaying = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
setBuffering(false)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
const handleStalled = () => {
|
||||
if (bufferingTimeout) clearTimeout(bufferingTimeout)
|
||||
bufferingTimeout = setTimeout(() => {
|
||||
setBuffering(true)
|
||||
}, 500) // Delay to avoid frequent buffering state changes
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setPlaying(false)
|
||||
setBuffering(false)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
|
||||
ref.current.addEventListener('timeupdate', handleTimeUpdate, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('durationchange', handleDurationChange, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('play', handlePlay, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('pause', handlePause, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('volumechange', handleVolumeChange, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('error', handleError, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('canplay', handleCanPlay, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('canplaythrough', handleCanPlayThrough, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('waiting', handleWaiting, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('playing', handlePlaying, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('stalled', handleStalled, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('ended', handleEnded, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
ref.current.addEventListener('volumechange', handleVolumeChange, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
clearTimeout(bufferingTimeout)
|
||||
}
|
||||
}, [ref, setVolume])
|
||||
|
||||
const play = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
if (ref.current.ended) {
|
||||
ref.current.currentTime = 0
|
||||
}
|
||||
|
||||
if (ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
|
||||
playWhenReadyRef.current = true
|
||||
} else {
|
||||
const promise = ref.current.play()
|
||||
if (promise !== undefined) {
|
||||
promise.catch(err => {
|
||||
console.error('Error playing video:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
const pause = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
ref.current.pause()
|
||||
playWhenReadyRef.current = false
|
||||
}, [ref])
|
||||
|
||||
const togglePlayPause = useCallback(() => {
|
||||
if (!ref.current) return
|
||||
|
||||
if (ref.current.paused) {
|
||||
play()
|
||||
} else {
|
||||
pause()
|
||||
}
|
||||
}, [ref, play, pause])
|
||||
|
||||
const changeMuted = useCallback(
|
||||
(newMuted: boolean | ((prev: boolean) => boolean)) => {
|
||||
if (!ref.current) return
|
||||
|
||||
const value =
|
||||
typeof newMuted === 'function' ? newMuted(ref.current.muted) : newMuted
|
||||
ref.current.muted = value
|
||||
},
|
||||
[ref],
|
||||
)
|
||||
|
||||
return {
|
||||
play,
|
||||
pause,
|
||||
togglePlayPause,
|
||||
duration,
|
||||
currentTime,
|
||||
playing,
|
||||
muted,
|
||||
changeMuted,
|
||||
buffering,
|
||||
error,
|
||||
canPlay,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTime(time: number) {
|
||||
if (isNaN(time)) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
time = Math.round(time)
|
||||
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = String(time % 60).padStart(2, '0')
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import React from 'react'
|
||||
|
||||
const Context = React.createContext(
|
||||
{} as {
|
||||
muted: boolean
|
||||
setMuted: (muted: boolean) => void
|
||||
},
|
||||
)
|
||||
const Context = React.createContext<{
|
||||
// native
|
||||
muted: boolean
|
||||
setMuted: React.Dispatch<React.SetStateAction<boolean>>
|
||||
// web
|
||||
volume: number
|
||||
setVolume: React.Dispatch<React.SetStateAction<number>>
|
||||
} | null>(null)
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [muted, setMuted] = React.useState(true)
|
||||
const [volume, setVolume] = React.useState(1)
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
muted,
|
||||
setMuted,
|
||||
volume,
|
||||
setVolume,
|
||||
}),
|
||||
[muted, setMuted],
|
||||
[muted, setMuted, volume, setVolume],
|
||||
)
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>
|
||||
@@ -28,5 +33,15 @@ export function useVideoVolumeState() {
|
||||
'useVideoVolumeState must be used within a VideoVolumeProvider',
|
||||
)
|
||||
}
|
||||
return context
|
||||
return [context.volume, context.setVolume] as const
|
||||
}
|
||||
|
||||
export function useVideoMuteState() {
|
||||
const context = React.useContext(Context)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useVideoMuteState must be used within a VideoVolumeProvider',
|
||||
)
|
||||
}
|
||||
return [context.muted, context.setMuted] as const
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user