Stop passing _ around in composer functions (#10256)

This commit is contained in:
Samuel Newman
2026-04-15 07:39:51 -07:00
committed by GitHub
parent bcbc114189
commit ecc78efb12
5 changed files with 129 additions and 137 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ export async function getServiceAuthToken({
return serviceAuth.token
}
export async function getVideoUploadLimits(agent: BskyAgent, _: I18n['_']) {
export async function getVideoUploadLimits(agent: BskyAgent, i18n: I18n) {
const token = await getServiceAuthToken({
agent,
lxm: 'app.bsky.video.getUploadLimits',
@@ -52,7 +52,7 @@ export async function getVideoUploadLimits(agent: BskyAgent, _: I18n['_']) {
throw new UploadLimitError(limits.message)
} else {
throw new UploadLimitError(
_(
i18n._(
msg`You have temporarily reached the limit for video uploads. Please try again later.`,
),
)
+6 -4
View File
@@ -16,19 +16,19 @@ export async function uploadVideo({
did,
setProgress,
signal,
_,
i18n,
}: {
video: CompressedVideo
agent: BskyAgent
did: string
setProgress: (progress: number) => void
signal: AbortSignal
_: I18n['_']
i18n: I18n
}) {
if (signal.aborted) {
throw new AbortError()
}
await getVideoUploadLimits(agent, _)
await getVideoUploadLimits(agent, i18n)
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
did,
@@ -69,7 +69,9 @@ export async function uploadVideo({
const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus
if (!responseBody.jobId) {
throw new ServerError(responseBody.error || _(msg`Failed to upload video`))
throw new ServerError(
responseBody.error || i18n._(msg`Failed to upload video`),
)
}
if (signal.aborted) {
+6 -6
View File
@@ -15,19 +15,19 @@ export async function uploadVideo({
did,
setProgress,
signal,
_,
i18n,
}: {
video: CompressedVideo
agent: BskyAgent
did: string
setProgress: (progress: number) => void
signal: AbortSignal
_: I18n['_']
i18n: I18n
}) {
if (signal.aborted) {
throw new AbortError()
}
await getVideoUploadLimits(agent, _)
await getVideoUploadLimits(agent, i18n)
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
did,
@@ -70,11 +70,11 @@ export async function uploadVideo({
) as AppBskyVideoDefs.JobStatus
resolve(uploadRes)
} else {
reject(new ServerError(_(msg`Failed to upload video`)))
reject(new ServerError(i18n._(msg`Failed to upload video`)))
}
}
xhr.onerror = () => {
reject(new ServerError(_(msg`Failed to upload video`)))
reject(new ServerError(i18n._(msg`Failed to upload video`)))
}
xhr.open('POST', uri)
xhr.setRequestHeader('Content-Type', video.mimeType)
@@ -84,7 +84,7 @@ export async function uploadVideo({
)
if (!res.jobId) {
throw new ServerError(res.error || _(msg`Failed to upload video`))
throw new ServerError(res.error || i18n._(msg`Failed to upload video`))
}
if (signal.aborted) {
+96 -106
View File
@@ -54,9 +54,8 @@ import {
type BskyAgent,
type RichText,
} from '@atproto/api'
import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
@@ -197,12 +196,13 @@ export const ComposePost = ({
cancelRef?: React.RefObject<CancelRef | null>
}) => {
const {currentAccount} = useSession()
const t = useTheme()
const ax = useAnalytics()
const agent = useAgent()
const queryClient = useQueryClient()
const currentDid = currentAccount!.did
const {closeComposer} = useComposerControls()
const {_} = useLingui()
const {t: l, i18n} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
@@ -313,7 +313,7 @@ export const ComposePost = ({
abortController,
},
})
processVideo(
void processVideo(
asset,
videoAction => {
composerDispatch({
@@ -328,10 +328,10 @@ export const ComposePost = ({
agent,
currentDid,
abortController.signal,
_,
i18n,
)
},
[_, agent, currentDid, composerDispatch],
[i18n, agent, currentDid, composerDispatch],
)
const onInitVideo = useNonReactiveCallback(() => {
@@ -460,7 +460,7 @@ export const ComposePost = ({
}
// Start video compression and upload
processVideo(
void processVideo(
asset,
videoAction => {
composerDispatch({
@@ -475,7 +475,7 @@ export const ComposePost = ({
agent,
currentDid,
abortController.signal,
_,
i18n,
)
} catch (e) {
logger.error('Failed to restore video from draft', {
@@ -484,7 +484,7 @@ export const ComposePost = ({
})
}
},
[_, agent, currentDid, composerDispatch],
[i18n, agent, currentDid, composerDispatch],
)
const handleSelectDraft = useCallback(
@@ -558,11 +558,11 @@ export const ComposePost = ({
const getDraftSaveError = useCallback(
(e: unknown): string => {
if (e instanceof AppBskyDraftCreateDraft.DraftLimitReachedError) {
return _(msg`You've reached the maximum number of drafts`)
return l`You've reached the maximum number of drafts`
}
return _(msg`Failed to save draft`)
return l`Failed to save draft`
},
[_],
[l],
)
const validateDraftTextOrError = useCallback((): boolean => {
@@ -571,14 +571,12 @@ export const ComposePost = ({
)
if (tooLong) {
setError(
_(
msg`One or more posts are too long to save as a draft. ${plural(MAX_DRAFT_GRAPHEME_LENGTH, {one: 'The maximum number of characters is # character.', other: 'The maximum number of characters is # characters.'})}`,
),
l`One or more posts are too long to save as a draft. ${plural(MAX_DRAFT_GRAPHEME_LENGTH, {one: 'The maximum number of characters is # character.', other: 'The maximum number of characters is # characters.'})}`,
)
return false
}
return true
}, [composerState.thread.posts, _])
}, [composerState.thread.posts, l])
const handleSaveDraft = useCallback(async () => {
setError('')
@@ -768,21 +766,21 @@ export const ComposePost = ({
const media = thread.posts[i].embed.media
if (media) {
if (media.type === 'images' && media.images.some(img => !img.alt)) {
return _(msg`One or more images is missing alt text.`)
return l`One or more images is missing alt text.`
}
if (media.type === 'gif' && !media.alt) {
return _(msg`One or more GIFs is missing alt text.`)
return l`One or more GIFs is missing alt text.`
}
if (
media.type === 'video' &&
media.video.status !== 'error' &&
!media.video.altText
) {
return _(msg`One or more videos is missing alt text.`)
return l`One or more videos is missing alt text.`
}
}
}
}, [thread, requireAltTextEnabled, _])
}, [thread, requireAltTextEnabled, l])
const canPost =
!missingAltError &&
@@ -895,11 +893,9 @@ export const ComposePost = ({
let err = cleanError(e.message)
if (err.includes('not locate record')) {
err = _(
msg`We're sorry! The post you are replying to has been deleted.`,
)
err = l`We're sorry! The post you are replying to has been deleted.`
} else if (e instanceof EmbeddingDisabledError) {
err = _(msg`This post's author has disabled quote posts.`)
err = l`This post's author has disabled quote posts.`
}
setError(err)
setIsPublishing(false)
@@ -979,14 +975,14 @@ export const ComposePost = ({
<Toast.Icon />
<Toast.Text>
{thread.posts.length > 1
? _(msg`Your posts were sent`)
? l`Your posts were sent`
: replyTo
? _(msg`Your reply was sent`)
: _(msg`Your post was sent`)}
? l`Your reply was sent`
: l`Your post was sent`}
</Toast.Text>
{postUri && (
<Toast.Action
label={_(msg`View post`)}
label={l`View post`}
onPress={() => {
const {host: name, rkey} = new AtUri(postUri)
navigation.navigate('PostThread', {name, rkey})
@@ -1001,7 +997,7 @@ export const ComposePost = ({
)
}, 500)
}, [
_,
l,
ax,
agent,
thread,
@@ -1026,7 +1022,7 @@ export const ComposePost = ({
// Preserves the referential identity passed to each post item.
// Avoids re-rendering all posts on each keystroke.
const onComposerPostPublish = useNonReactiveCallback(() => {
onPressPublish()
void onPressPublish()
})
useEffect(() => {
@@ -1047,7 +1043,7 @@ export const ComposePost = ({
setPublishOnUpload(false)
} else if (uploadingVideos === 0) {
setPublishOnUpload(false)
onPressPublish()
void onPressPublish()
}
}
}, [thread.posts, onPressPublish, publishOnUpload])
@@ -1189,7 +1185,13 @@ export const ComposePost = ({
layout={native(LinearTransition)}
onScroll={scrollHandler}
contentContainerStyle={a.flex_grow}
style={a.flex_1}
style={[
a.flex_1,
web({
scrollbarGutter: 'stable',
scrollbarColor: `${t.palette.contrast_200} transparent`,
}),
]}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
@@ -1224,9 +1226,9 @@ export const ComposePost = ({
{replyTo ? (
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
title={l`Discard draft?`}
description=""
confirmButtonCta={_(msg`Discard`)}
confirmButtonCta={l`Discard`}
confirmButtonColor="negative"
onConfirm={handleDiscard}
/>
@@ -1264,21 +1266,17 @@ export const ComposePost = ({
<Prompt.Actions>
{allPostsWithinLimit && (
<Prompt.Action
cta={
composerState.draftId
? _(msg`Save changes`)
: _(msg`Save draft`)
}
cta={composerState.draftId ? l`Save changes` : l`Save draft`}
onPress={handleSaveDraft}
color="primary"
/>
)}
<Prompt.Action
cta={_(msg`Discard`)}
cta={l`Discard`}
onPress={handleDiscard}
color="negative_subtle"
/>
<Prompt.Cancel cta={_(msg`Keep editing`)} />
<Prompt.Cancel cta={l`Keep editing`} />
</Prompt.Actions>
</Prompt.Outer>
)}
@@ -1320,16 +1318,16 @@ let ComposerPost = memo(function ComposerPost({
}) {
const {currentAccount} = useSession()
const currentDid = currentAccount!.did
const {_} = useLingui()
const {t: l} = useLingui()
const {data: currentProfile} = useProfileQuery({did: currentDid})
const richtext = post.richtext
const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
const forceMinHeight = IS_WEB && isTextOnly && isActive
const selectTextInputPlaceholder = isReply
? isFirstPost
? _(msg`Write your reply`)
: _(msg`Add another post`)
: _(msg`What's up?`)
? l`Write your reply`
: l`Add another post`
: l`What's up?`
const discardPromptControl = Prompt.usePromptControl()
const dispatchPost = useCallback(
@@ -1369,7 +1367,7 @@ let ComposerPost = memo(function ComposerPost({
if (IS_NATIVE) return // web only
const [mimeType] = uri.slice('data:'.length).split(';')
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
Toast.show(_(msg`Unsupported video type: ${mimeType}`), {
Toast.show(l`Unsupported video type: ${mimeType}`, {
type: 'error',
})
return
@@ -1384,7 +1382,7 @@ let ComposerPost = memo(function ComposerPost({
onImageAdd([res])
}
},
[post.id, onSelectVideo, onImageAdd, _],
[post.id, onSelectVideo, onImageAdd, l],
)
useHideKeyboardOnBackground()
@@ -1429,19 +1427,20 @@ let ComposerPost = memo(function ComposerPost({
onError={onError}
onPressPublish={onPublish}
accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_(
msg`Compose posts up to ${plural(MAX_GRAPHEME_LENGTH || 0, {
accessibilityLabel={l`Write post`}
accessibilityHint={l`Compose posts up to ${plural(
MAX_GRAPHEME_LENGTH || 0,
{
other: '# characters',
})} in length`,
)}
},
)} in length`}
/>
</View>
{canRemovePost && isActive && (
<>
<Button
label={_(msg`Delete post`)}
label={l`Delete post`}
size="small"
color="secondary"
variant="ghost"
@@ -1466,15 +1465,15 @@ let ComposerPost = memo(function ComposerPost({
</Button>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard post?`)}
description={_(msg`Are you sure you'd like to discard this post?`)}
title={l`Discard post?`}
description={l`Are you sure you'd like to discard this post?`}
onConfirm={() => {
dispatch({
type: 'remove_post',
postId: post.id,
})
}}
confirmButtonCta={_(msg`Discard`)}
confirmButtonCta={l`Discard`}
confirmButtonColor="negative"
/>
</>
@@ -1531,7 +1530,8 @@ function ComposerTopBar({
children?: React.ReactNode
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
return (
<Animated.View
style={topBarAnimatedStyle}
@@ -1544,7 +1544,7 @@ function ComposerTopBar({
IS_LIQUID_GLASS ? [a.px_lg, a.pt_lg, a.pb_md] : [a.p_sm],
]}>
<Button
label={_(msg`Cancel`)}
label={l`Cancel`}
variant="ghost"
color="primary"
shape="default"
@@ -1552,9 +1552,7 @@ function ComposerTopBar({
style={[{paddingLeft: 7, paddingRight: 7}]}
hoverStyle={[a.bg_transparent, {opacity: 0.5}]}
onPress={onCancel}
accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}>
accessibilityHint={l`Closes post composer and discards post draft`}>
<ButtonText style={[a.text_md]} maxFontSizeMultiplier={2}>
<Trans>Cancel</Trans>
</ButtonText>
@@ -1588,35 +1586,27 @@ function ComposerTopBar({
label={
isReply
? isThread
? _(
msg({
message: 'Publish replies',
comment:
'Accessibility label for button to publish multiple replies in a thread',
}),
)
: _(
msg({
message: 'Publish reply',
comment:
'Accessibility label for button to publish a single reply',
}),
)
? l({
message: 'Publish replies',
comment:
'Accessibility label for button to publish multiple replies in a thread',
})
: l({
message: 'Publish reply',
comment:
'Accessibility label for button to publish a single reply',
})
: isThread
? _(
msg({
message: 'Publish posts',
comment:
'Accessibility label for button to publish multiple posts in a thread',
}),
)
: _(
msg({
message: 'Publish post',
comment:
'Accessibility label for button to publish a single post',
}),
)
? l({
message: 'Publish posts',
comment:
'Accessibility label for button to publish multiple posts in a thread',
})
: l({
message: 'Publish post',
comment:
'Accessibility label for button to publish a single post',
})
}
color="primary"
size="small"
@@ -1851,7 +1841,7 @@ function ComposerFooter({
openGallery?: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const {isMobile} = useWebMediaQueries()
/*
* Once we've allowed a certain type of asset to be selected, we don't allow
@@ -1979,8 +1969,8 @@ function ComposerFooter({
<Button
onPress={onEmojiButtonPress}
style={a.p_sm}
label={_(msg`Open emoji picker`)}
accessibilityHint={_(msg`Opens emoji picker`)}
label={l`Open emoji picker`}
accessibilityHint={l`Opens emoji picker`}
variant="ghost"
shape="round"
color="primary">
@@ -1994,7 +1984,7 @@ function ComposerFooter({
<View style={[a.flex_row, a.align_center, a.justify_between]}>
{showAddButton && (
<Button
label={_(msg`Add another post to thread`)}
label={l`Add another post to thread`}
onPress={onAddPost}
style={[a.p_sm]}
variant="ghost"
@@ -2276,7 +2266,7 @@ function ErrorBanner({
clearVideo: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const videoError =
videoState.status === 'error' ? videoState.error : undefined
@@ -2311,7 +2301,7 @@ function ErrorBanner({
{error}
</Text>
<Button
label={_(msg`Dismiss error`)}
label={l`Dismiss error`}
size="tiny"
color="secondary"
variant="ghost"
@@ -2358,7 +2348,7 @@ function ToolbarWrapper({
function VideoUploadToolbar({state}: {state: VideoState}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const progress = state.progress
const shouldRotate =
state.status === 'processing' && (progress === 0 || progress === 1)
@@ -2390,34 +2380,34 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
switch (state.status) {
case 'compressing':
if (isGif) {
text = _(msg`Compressing GIF...`)
text = l`Compressing GIF...`
} else {
text = _(msg`Compressing video...`)
text = l`Compressing video...`
}
break
case 'uploading':
if (isGif) {
text = _(msg`Uploading GIF...`)
text = l`Uploading GIF...`
} else {
text = _(msg`Uploading video...`)
text = l`Uploading video...`
}
break
case 'processing':
if (isGif) {
text = _(msg`Processing GIF...`)
text = l`Processing GIF...`
} else {
text = _(msg`Processing video...`)
text = l`Processing video...`
}
break
case 'error':
text = _(msg`Error`)
text = l`Error`
wheelProgress = 100
break
case 'done':
if (isGif) {
text = _(msg`GIF uploaded`)
text = l`GIF uploaded`
} else {
text = _(msg`Video uploaded`)
text = l`Video uploaded`
}
break
}
+19 -19
View File
@@ -263,7 +263,7 @@ export async function processVideo(
agent: BskyAgent,
did: string,
signal: AbortSignal,
_: I18n['_'],
i18n: I18n,
) {
let video: CompressedVideo | undefined
try {
@@ -274,7 +274,7 @@ export async function processVideo(
signal,
})
} catch (e) {
const message = getCompressErrorMessage(e, _)
const message = getCompressErrorMessage(e, i18n)
if (message !== null) {
dispatch({
type: 'to_error',
@@ -297,13 +297,13 @@ export async function processVideo(
agent,
did,
signal,
_,
i18n,
setProgress: p => {
dispatch({type: 'update_progress', progress: p, signal})
},
})
} catch (e) {
const message = getUploadErrorMessage(e, _)
const message = getUploadErrorMessage(e, i18n)
if (message !== null) {
dispatch({
type: 'to_error',
@@ -355,7 +355,7 @@ export async function processVideo(
logger.error('Error processing video', {safeMessage: e})
dispatch({
type: 'to_error',
error: _(msg`Video failed to process`),
error: i18n._(msg`Video failed to process`),
signal,
})
return // Exit async loop
@@ -387,20 +387,20 @@ export async function processVideo(
}
}
function getCompressErrorMessage(e: unknown, _: I18n['_']): string | null {
function getCompressErrorMessage(e: unknown, i18n: I18n): string | null {
if (e instanceof AbortError) {
return null
}
if (e instanceof VideoTooLargeError) {
return _(
return i18n._(
msg`The selected video is larger than 100 MB. Please try again with a smaller file.`,
)
}
logger.error('Error compressing video', {safeMessage: e})
return _(msg`An error occurred while compressing the video.`)
return i18n._(msg`An error occurred while compressing the video.`)
}
function getUploadErrorMessage(e: unknown, _: I18n['_']): string | null {
function getUploadErrorMessage(e: unknown, i18n: I18n): string | null {
if (e instanceof AbortError) {
return null
}
@@ -408,38 +408,38 @@ function getUploadErrorMessage(e: unknown, _: I18n['_']): string | null {
// https://github.com/bluesky-social/tango/blob/lumi/lumi/worker/permissions.go#L77
switch (e.message) {
case 'User is not allowed to upload videos':
return _(msg`You are not allowed to upload videos.`)
return i18n._(msg`You are not allowed to upload videos.`)
case 'Uploading is disabled at the moment':
return _(
return i18n._(
msg`Hold up! Were gradually giving access to video, and youre still waiting in line. Check back soon!`,
)
case "Failed to get user's upload stats":
return _(
return i18n._(
msg`We were unable to determine if you are allowed to upload videos. Please try again.`,
)
case 'User has exceeded daily upload bytes limit':
return _(
return i18n._(
msg`You've reached your daily limit for video uploads (too many bytes)`,
)
case 'User has exceeded daily upload videos limit':
return _(
return i18n._(
msg`You've reached your daily limit for video uploads (too many videos)`,
)
case 'Account is not old enough to upload videos':
return _(
return i18n._(
msg`Your account is not yet old enough to upload videos. Please try again later.`,
)
case 'file size (100000001 bytes) is larger than the maximum allowed size (100000000 bytes)':
return _(
return i18n._(
msg`The selected video is larger than 100 MB. Please try again with a smaller file.`,
)
case 'Confirm your email address to upload videos':
return _(msg`Please confirm your email address to upload videos.`)
return i18n._(msg`Please confirm your email address to upload videos.`)
}
}
if (isNetworkError(e)) {
return _(
return i18n._(
msg`An error occurred while uploading the video. Please check your internet connection and try again.`,
)
} else {
@@ -448,5 +448,5 @@ function getUploadErrorMessage(e: unknown, _: I18n['_']): string | null {
}
const message = e instanceof Error ? e.message : ''
return _(msg`An error occurred while uploading the video. ${message}`)
return i18n._(msg`An error occurred while uploading the video. ${message}`)
}