mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-27 03:32:55 +00:00
feat(app): long-press Send to queue or send now on touch screens (#7897)
Touch users had no way to reach what Cmd/Ctrl+Enter does: queue when Default send is Steer or Interrupt, send now when it is Queue. Holding the Send button now runs that same action on every platform, with a haptic on native and an accessibility hint naming it. Tapping is unchanged. Both paths go through runAlternateSendAction, which now falls back to the default send when no turn is running, so holding Send while idle still sends and the keyboard path no longer carries its own gate. Co-authored-by: bjspi <43368639+bjspi@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,9 @@ import {
|
||||
expectComposerVisible,
|
||||
cancelAgent,
|
||||
composerLocator,
|
||||
expectQueuedMessageButton,
|
||||
fillComposerDraft,
|
||||
holdSendButton,
|
||||
sendDraftToQueue,
|
||||
startRunningMockAgent,
|
||||
submitMessage,
|
||||
@@ -1290,6 +1292,49 @@ test.describe("Agent message submission", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("queues a draft by holding Send while a tap still steers", async ({ page }, testInfo) => {
|
||||
test.setTimeout(120_000);
|
||||
const gate = await gateNextAgentMessage(page);
|
||||
const agent = await startRunningMockAgent(page, {
|
||||
prefix: `hold-send-queue-${testInfo.workerIndex}-`,
|
||||
model: "one-minute-stream",
|
||||
prompt: "Keep this turn active while the user holds Send.",
|
||||
});
|
||||
try {
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentReadyToInterrupt(page);
|
||||
|
||||
const queuedPrompt = "Queue this follow-up by holding Send.";
|
||||
await test.step("holding Send queues the draft", async () => {
|
||||
await fillComposerDraft(page, queuedPrompt);
|
||||
await holdSendButton(page, "Send and steer");
|
||||
await expectQueuedMessageButton(page);
|
||||
await expectComposerDraft(page, "");
|
||||
await expectAgentReadyToInterrupt(page);
|
||||
await expect(
|
||||
page.getByTestId("user-message").filter({ hasText: queuedPrompt }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("tapping Send still steers", async () => {
|
||||
const steeredPrompt = "Steer this turn with a tap.";
|
||||
await fillComposerDraft(page, steeredPrompt);
|
||||
await page.getByRole("button", { name: "Send and steer", exact: true }).click();
|
||||
|
||||
const request = await gate.waitForRequest();
|
||||
expect(request.text).toBe(steeredPrompt);
|
||||
expect(request.activeTurnBehavior).toBe("steer");
|
||||
gate.accept();
|
||||
await expect(
|
||||
page.getByTestId("user-message").filter({ hasText: steeredPrompt }),
|
||||
).toHaveCount(1);
|
||||
await expectQueuedMessageButton(page);
|
||||
});
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("replays Claude-shaped steering inside one active turn", async ({ page }, testInfo) => {
|
||||
test.setTimeout(120_000);
|
||||
await replaySteeredSleepTurnInBrowser(page, testInfo, "claude");
|
||||
|
||||
@@ -87,6 +87,14 @@ export async function sendDraftToQueue(page: Page): Promise<void> {
|
||||
await composerInput(page).press("Control+Enter");
|
||||
}
|
||||
|
||||
/** Long-press the Send button: the composer takes the draft while the pointer is still down. */
|
||||
export async function holdSendButton(page: Page, label: string): Promise<void> {
|
||||
await page.getByRole("button", { name: label, exact: true }).hover();
|
||||
await page.mouse.down();
|
||||
await expect(composerInput(page)).toHaveValue("", { timeout: 5_000 });
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
export async function expectQueuedMessageButton(page: Page): Promise<void> {
|
||||
await expect(page.getByRole("button", { name: "Send queued message now" })).toBeVisible({
|
||||
timeout: 10_000,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import { ArrowUp, Mic, MicOff, CornerDownLeft, Plus, Square } from "lucide-react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { useDictation } from "@/hooks/use-dictation";
|
||||
import { hasFinePointer } from "@/hooks/use-fine-pointer";
|
||||
import { DictationOverlay } from "@/components/dictation-controls";
|
||||
@@ -57,7 +58,7 @@ import { formatShortcut, type ShortcutKey } from "@/utils/format-shortcut";
|
||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
|
||||
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { isNative, isWeb } from "@/constants/platform";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useComposerKeyboardScope } from "@/composer/keyboard-scope";
|
||||
import { RenderProfile } from "@/utils/render-profiler";
|
||||
@@ -76,6 +77,7 @@ const ComposerTextInput = withUnistyles(EditingTextInput, (theme) => ({
|
||||
}));
|
||||
import {
|
||||
resolveSendTooltipLabel,
|
||||
resolveSubmitAccessibilityHint,
|
||||
resolveSubmitAccessibilityLabel,
|
||||
resolveVoiceAccessibilityLabel,
|
||||
resolveVoiceTooltipText,
|
||||
@@ -83,6 +85,7 @@ import {
|
||||
import {
|
||||
applyDictationTranscript,
|
||||
computeCanStartDictation,
|
||||
resolveAlternateSendAction,
|
||||
resolveComposerSurfacePresentation,
|
||||
resolveComposerSurfaceText,
|
||||
resolveRetainedSelection,
|
||||
@@ -399,8 +402,6 @@ interface DesktopKeyPressContext {
|
||||
onKeyPressCallback: ((event: ComposerKeyPressEvent) => boolean) | undefined;
|
||||
input: ComposerKeyPressEvent["input"];
|
||||
submitOnEnter: boolean;
|
||||
isAgentRunning: boolean;
|
||||
onQueue: ((payload: MessagePayload) => void) | undefined;
|
||||
isSubmitDisabled: boolean;
|
||||
isSubmitLoading: boolean;
|
||||
disabled: boolean;
|
||||
@@ -430,15 +431,12 @@ function handleDesktopKeyPressImpl(
|
||||
if (!ctx.submitOnEnter) return;
|
||||
if (shiftKey) return;
|
||||
|
||||
if ((metaKey || ctrlKey) && ctx.isAgentRunning && ctx.onQueue) {
|
||||
if (ctx.isSubmitDisabled || ctx.isSubmitLoading || ctx.disabled) return;
|
||||
event.preventDefault();
|
||||
if (ctx.isSubmitDisabled || ctx.isSubmitLoading || ctx.disabled) return;
|
||||
event.preventDefault();
|
||||
if (metaKey || ctrlKey) {
|
||||
ctx.handleAlternateSendAction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.isSubmitDisabled || ctx.isSubmitLoading || ctx.disabled) return;
|
||||
event.preventDefault();
|
||||
ctx.handleDefaultSendAction();
|
||||
}
|
||||
|
||||
@@ -769,8 +767,10 @@ function SendButtonTooltip({
|
||||
canPressLoadingButton,
|
||||
onSubmitLoadingPress,
|
||||
onDefaultSendAction,
|
||||
onLongPressSend,
|
||||
isSendButtonDisabled,
|
||||
submitAccessibilityLabel,
|
||||
submitAccessibilityHint,
|
||||
sendButtonCombinedStyle,
|
||||
isSubmitLoading,
|
||||
submitIcon,
|
||||
@@ -784,8 +784,10 @@ function SendButtonTooltip({
|
||||
canPressLoadingButton: boolean;
|
||||
onSubmitLoadingPress: (() => void) | undefined;
|
||||
onDefaultSendAction: () => void;
|
||||
onLongPressSend: () => void;
|
||||
isSendButtonDisabled: boolean;
|
||||
submitAccessibilityLabel: string;
|
||||
submitAccessibilityHint: string | undefined;
|
||||
sendButtonCombinedStyle: React.ComponentProps<typeof TooltipTrigger>["style"];
|
||||
isSubmitLoading: boolean;
|
||||
submitIcon: "arrow" | "return";
|
||||
@@ -800,8 +802,10 @@ function SendButtonTooltip({
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger
|
||||
onPress={canPressLoadingButton ? onSubmitLoadingPress : onDefaultSendAction}
|
||||
onLongPress={canPressLoadingButton ? undefined : onLongPressSend}
|
||||
disabled={isSendButtonDisabled}
|
||||
accessibilityLabel={submitAccessibilityLabel}
|
||||
accessibilityHint={submitAccessibilityHint}
|
||||
accessibilityRole="button"
|
||||
testID={submitButtonTestID}
|
||||
style={sendButtonCombinedStyle}
|
||||
@@ -1705,6 +1709,19 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
});
|
||||
}, [defaultSendBehavior, isAgentRunning, handleSendMessage, handleQueueMessage, onQueue]);
|
||||
|
||||
const alternateSendAction = resolveAlternateSendAction({
|
||||
defaultSendBehavior,
|
||||
isAgentRunning,
|
||||
onQueue,
|
||||
});
|
||||
|
||||
const handleLongPressSend = useCallback(() => {
|
||||
if (alternateSendAction && isNative) {
|
||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||
}
|
||||
handleAlternateSendAction();
|
||||
}, [alternateSendAction, handleAlternateSendAction]);
|
||||
|
||||
usePasteImagesEffect({
|
||||
textarea: webTextArea,
|
||||
isConnected,
|
||||
@@ -1738,8 +1755,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
selectionRef.current,
|
||||
),
|
||||
submitOnEnter: shouldSubmitOnEnter,
|
||||
isAgentRunning,
|
||||
onQueue,
|
||||
isSubmitDisabled,
|
||||
isSubmitLoading,
|
||||
disabled,
|
||||
@@ -1751,8 +1766,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
shouldHandleWebKeyPress,
|
||||
onKeyPressCallback,
|
||||
shouldSubmitOnEnter,
|
||||
isAgentRunning,
|
||||
onQueue,
|
||||
isSubmitDisabled,
|
||||
isSubmitLoading,
|
||||
disabled,
|
||||
@@ -1792,6 +1805,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
isAgentRunning,
|
||||
t,
|
||||
});
|
||||
const submitAccessibilityHint = resolveSubmitAccessibilityHint({
|
||||
canPressLoadingButton,
|
||||
alternateSendAction,
|
||||
t,
|
||||
});
|
||||
|
||||
const voiceButtonAccessibilityLabel = resolveVoiceAccessibilityLabel({
|
||||
isRealtimeVoiceForCurrentAgent,
|
||||
@@ -1952,8 +1970,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
canPressLoadingButton={canPressLoadingButton}
|
||||
onSubmitLoadingPress={onSubmitLoadingPress}
|
||||
onDefaultSendAction={handleDefaultSendAction}
|
||||
onLongPressSend={handleLongPressSend}
|
||||
isSendButtonDisabled={isPresentedSendDisabled}
|
||||
submitAccessibilityLabel={submitAccessibilityLabel}
|
||||
submitAccessibilityHint={submitAccessibilityHint}
|
||||
sendButtonCombinedStyle={sendButtonCombinedStyle}
|
||||
isSubmitLoading={isSubmitLoading}
|
||||
submitIcon={submitIcon}
|
||||
@@ -1971,8 +1991,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
canPressLoadingButton,
|
||||
onSubmitLoadingPress,
|
||||
handleDefaultSendAction,
|
||||
handleLongPressSend,
|
||||
isPresentedSendDisabled,
|
||||
submitAccessibilityLabel,
|
||||
submitAccessibilityHint,
|
||||
sendButtonCombinedStyle,
|
||||
isSubmitLoading,
|
||||
submitIcon,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveSendTooltipLabel,
|
||||
resolveSubmitAccessibilityHint,
|
||||
resolveSubmitAccessibilityLabel,
|
||||
resolveVoiceAccessibilityLabel,
|
||||
resolveVoiceTooltipText,
|
||||
@@ -12,6 +13,8 @@ const translations: Record<string, string> = {
|
||||
"composer.input.sendAndInterrupt": "Send and interrupt",
|
||||
"composer.input.sendAndSteer": "Send and steer",
|
||||
"composer.input.sendMessage": "Send message",
|
||||
"composer.input.longPressToQueue": "Long press to queue",
|
||||
"composer.input.longPressToSendNow": "Long press to send now",
|
||||
"composer.input.queue": "Queue",
|
||||
"composer.input.send": "Send",
|
||||
"composer.voice.unmuteVoiceMode": "Unmute Voice mode",
|
||||
@@ -26,6 +29,37 @@ const translations: Record<string, string> = {
|
||||
const t = ((key: string) => translations[key] ?? key) as never;
|
||||
|
||||
describe("composer input labels", () => {
|
||||
it("names the long-press action in the submit hint only when one exists", () => {
|
||||
expect(
|
||||
resolveSubmitAccessibilityHint({
|
||||
canPressLoadingButton: false,
|
||||
alternateSendAction: "queue",
|
||||
t,
|
||||
}),
|
||||
).toBe("Long press to queue");
|
||||
expect(
|
||||
resolveSubmitAccessibilityHint({
|
||||
canPressLoadingButton: false,
|
||||
alternateSendAction: "send",
|
||||
t,
|
||||
}),
|
||||
).toBe("Long press to send now");
|
||||
expect(
|
||||
resolveSubmitAccessibilityHint({
|
||||
canPressLoadingButton: false,
|
||||
alternateSendAction: null,
|
||||
t,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveSubmitAccessibilityHint({
|
||||
canPressLoadingButton: true,
|
||||
alternateSendAction: "queue",
|
||||
t,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resolves submit accessibility labels from translations", () => {
|
||||
expect(
|
||||
resolveSubmitAccessibilityLabel({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import type { SendBehavior } from "@/hooks/use-settings/storage";
|
||||
import type { AlternateSendAction } from "./state";
|
||||
|
||||
export function resolveSubmitAccessibilityLabel(input: {
|
||||
submitButtonAccessibilityLabel: string | undefined;
|
||||
@@ -22,6 +23,19 @@ export function resolveSubmitAccessibilityLabel(input: {
|
||||
return input.t("composer.input.sendMessage");
|
||||
}
|
||||
|
||||
export function resolveSubmitAccessibilityHint(input: {
|
||||
canPressLoadingButton: boolean;
|
||||
alternateSendAction: AlternateSendAction | null;
|
||||
t: TFunction;
|
||||
}): string | undefined {
|
||||
if (input.canPressLoadingButton || !input.alternateSendAction) return undefined;
|
||||
return input.t(
|
||||
input.alternateSendAction === "queue"
|
||||
? "composer.input.longPressToQueue"
|
||||
: "composer.input.longPressToSendNow",
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveVoiceAccessibilityLabel(input: {
|
||||
isRealtimeVoiceForCurrentAgent: boolean;
|
||||
isMuted: boolean;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveComposerSurfaceText,
|
||||
resolveGlyphRailOffset,
|
||||
resolveRetainedSelection,
|
||||
resolveAlternateSendAction,
|
||||
runAlternateSendAction,
|
||||
runDefaultSendAction,
|
||||
runMessageInputKeyboardAction,
|
||||
@@ -276,6 +277,47 @@ describe("composer send behavior", () => {
|
||||
expect(defaultAction.calls).toEqual(["queue"]);
|
||||
expect(alternateAction.calls).toEqual(["send"]);
|
||||
});
|
||||
|
||||
it("sends with the alternate action while no turn is running", () => {
|
||||
for (const defaultSendBehavior of ["steer", "interrupt", "queue"] as const) {
|
||||
const alternateAction = actions();
|
||||
runAlternateSendAction({
|
||||
defaultSendBehavior,
|
||||
isAgentRunning: false,
|
||||
onQueue: alternateAction.onQueue,
|
||||
handleSendMessage: alternateAction.handleSendMessage,
|
||||
handleQueueMessage: alternateAction.handleQueueMessage,
|
||||
});
|
||||
expect(alternateAction.calls).toEqual(["send"]);
|
||||
}
|
||||
});
|
||||
|
||||
it("offers an alternate action only while a turn runs and the composer can queue", () => {
|
||||
const onQueue = () => undefined;
|
||||
expect(
|
||||
resolveAlternateSendAction({ defaultSendBehavior: "steer", isAgentRunning: true, onQueue }),
|
||||
).toBe("queue");
|
||||
expect(
|
||||
resolveAlternateSendAction({
|
||||
defaultSendBehavior: "interrupt",
|
||||
isAgentRunning: true,
|
||||
onQueue,
|
||||
}),
|
||||
).toBe("queue");
|
||||
expect(
|
||||
resolveAlternateSendAction({ defaultSendBehavior: "queue", isAgentRunning: true, onQueue }),
|
||||
).toBe("send");
|
||||
expect(
|
||||
resolveAlternateSendAction({ defaultSendBehavior: "steer", isAgentRunning: false, onQueue }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveAlternateSendAction({
|
||||
defaultSendBehavior: "queue",
|
||||
isAgentRunning: true,
|
||||
onQueue: undefined,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("stopRealtimeVoice", () => {
|
||||
|
||||
@@ -125,14 +125,30 @@ export function runDefaultSendAction(ctx: SendActionContext): void {
|
||||
ctx.handleSendMessage();
|
||||
}
|
||||
|
||||
export type AlternateSendAction = "queue" | "send";
|
||||
|
||||
/**
|
||||
* What Mod+Enter and a long-press on Send do instead of the default send. Null while
|
||||
* no turn is running, where both fall back to the default send.
|
||||
*/
|
||||
export function resolveAlternateSendAction(
|
||||
ctx: Pick<SendActionContext, "defaultSendBehavior" | "isAgentRunning" | "onQueue">,
|
||||
): AlternateSendAction | null {
|
||||
if (!ctx.isAgentRunning || !ctx.onQueue) return null;
|
||||
return ctx.defaultSendBehavior === "queue" ? "send" : "queue";
|
||||
}
|
||||
|
||||
export function runAlternateSendAction(ctx: SendActionContext): void {
|
||||
if (ctx.defaultSendBehavior === "queue") {
|
||||
const action = resolveAlternateSendAction(ctx);
|
||||
if (action === "queue") {
|
||||
ctx.handleQueueMessage();
|
||||
return;
|
||||
}
|
||||
if (action === "send") {
|
||||
ctx.handleSendMessage();
|
||||
return;
|
||||
}
|
||||
if (ctx.isAgentRunning && ctx.onQueue) {
|
||||
ctx.handleQueueMessage();
|
||||
}
|
||||
runDefaultSendAction(ctx);
|
||||
}
|
||||
|
||||
export function runMessageInputKeyboardAction(
|
||||
|
||||
@@ -133,6 +133,8 @@ export const ar: TranslationResources = {
|
||||
sendAndInterrupt: "إرسال ومقاطعة",
|
||||
sendAndSteer: "إرسال وتوجيه",
|
||||
sendMessage: "أرسل رسالة",
|
||||
longPressToQueue: "اضغط مطولًا لإضافتها إلى الطابور",
|
||||
longPressToSendNow: "اضغط مطولًا للإرسال الآن",
|
||||
queue: "طابور",
|
||||
send: "يرسل",
|
||||
},
|
||||
|
||||
@@ -129,6 +129,8 @@ export const en = {
|
||||
sendAndInterrupt: "Send and interrupt",
|
||||
sendAndSteer: "Send and steer",
|
||||
sendMessage: "Send message",
|
||||
longPressToQueue: "Long press to queue",
|
||||
longPressToSendNow: "Long press to send now",
|
||||
queue: "Queue",
|
||||
send: "Send",
|
||||
},
|
||||
|
||||
@@ -134,6 +134,8 @@ export const es: TranslationResources = {
|
||||
sendAndInterrupt: "Enviar e interrumpir",
|
||||
sendAndSteer: "Enviar y orientar",
|
||||
sendMessage: "enviar mensaje",
|
||||
longPressToQueue: "Mantén pulsado para poner en cola",
|
||||
longPressToSendNow: "Mantén pulsado para enviar ahora",
|
||||
queue: "Cola",
|
||||
send: "Enviar",
|
||||
},
|
||||
|
||||
@@ -135,6 +135,8 @@ export const fr: TranslationResources = {
|
||||
sendAndInterrupt: "Envoyer et interrompre",
|
||||
sendAndSteer: "Envoyer et réorienter",
|
||||
sendMessage: "Envoyer le message",
|
||||
longPressToQueue: "Appui long pour mettre en file d’attente",
|
||||
longPressToSendNow: "Appui long pour envoyer maintenant",
|
||||
queue: "Mettre en file d’attente",
|
||||
send: "Envoyer",
|
||||
},
|
||||
|
||||
@@ -134,6 +134,8 @@ export const ja: TranslationResources = {
|
||||
sendAndInterrupt: "送信して中断",
|
||||
sendAndSteer: "送信して指示を追加",
|
||||
sendMessage: "メッセージを送信",
|
||||
longPressToQueue: "長押しでキューに追加",
|
||||
longPressToSendNow: "長押しで今すぐ送信",
|
||||
queue: "キュー",
|
||||
send: "送信",
|
||||
},
|
||||
|
||||
@@ -133,6 +133,8 @@ export const ko: TranslationResources = {
|
||||
sendAndInterrupt: "보내고 중단",
|
||||
sendAndSteer: "보내고 지시 추가",
|
||||
sendMessage: "메시지 보내기",
|
||||
longPressToQueue: "길게 눌러 대기열에 추가",
|
||||
longPressToSendNow: "길게 눌러 지금 보내기",
|
||||
queue: "대기열",
|
||||
send: "보내기",
|
||||
},
|
||||
|
||||
@@ -134,6 +134,8 @@ export const ptBR: TranslationResources = {
|
||||
sendAndInterrupt: "Enviar e interromper",
|
||||
sendAndSteer: "Enviar e orientar",
|
||||
sendMessage: "Enviar mensagem",
|
||||
longPressToQueue: "Mantenha pressionado para enfileirar",
|
||||
longPressToSendNow: "Mantenha pressionado para enviar agora",
|
||||
queue: "Fila",
|
||||
send: "Enviar",
|
||||
},
|
||||
|
||||
@@ -134,6 +134,8 @@ export const ru: TranslationResources = {
|
||||
sendAndInterrupt: "Отправить и прервать",
|
||||
sendAndSteer: "Отправить и направить",
|
||||
sendMessage: "Отправить сообщение",
|
||||
longPressToQueue: "Удерживайте, чтобы поставить в очередь",
|
||||
longPressToSendNow: "Удерживайте, чтобы отправить сейчас",
|
||||
queue: "Очередь",
|
||||
send: "Отправить",
|
||||
},
|
||||
|
||||
@@ -133,6 +133,8 @@ export const zhCN: TranslationResources = {
|
||||
sendAndInterrupt: "发送并中断",
|
||||
sendAndSteer: "发送并引导",
|
||||
sendMessage: "发送消息",
|
||||
longPressToQueue: "长按加入队列",
|
||||
longPressToSendNow: "长按立即发送",
|
||||
queue: "排队",
|
||||
send: "发送",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user