Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion components/chat/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ const fieldName = `chat-message-${Math.random().toString(36).slice(2, 10)}`;
]"
@keydown.enter="onEnter"
/>
<span
v-if="showRemaining"
class="shrink-0 self-center font-mono text-[0.6rem] tabular-nums leading-none"
:class="
remainingCharacters < 0
? 'text-destructive'
: 'text-muted-foreground/70'
"
>
{{ remainingCharacters }}
</span>
<Button
type="submit"
size="sm"
Expand Down Expand Up @@ -155,6 +166,10 @@ import * as z from "zod";
import { useForm } from "vee-validate";
import { toTypedSchema } from "~/utilities/vee-validate-zod";
import { chatEnterAction } from "~/utilities/chatInputKeys";
import { toastChatError } from "~/utilities/chatErrors";
import { CHAT_MESSAGE_MAX_LENGTH } from "~/constants/chat";

const REMAINING_HINT_AT = 200;

export interface ChatInputChannel {
value: string;
Expand Down Expand Up @@ -201,7 +216,7 @@ export default {
form: useForm({
validationSchema: toTypedSchema(
z.object({
message: z.string().min(1),
message: z.string().trim().min(1).max(CHAT_MESSAGE_MAX_LENGTH),
}),
),
}),
Expand All @@ -211,6 +226,14 @@ export default {
hasChannels() {
return this.channels.length > 1;
},
remainingCharacters(): number {
const message = this.form.values.message?.trim() ?? "";

return CHAT_MESSAGE_MAX_LENGTH - message.length;
},
showRemaining(): boolean {
return this.remainingCharacters <= REMAINING_HINT_AT;
},
activeChannelValue() {
return this.destination ?? this.channels[0]?.value;
},
Expand Down Expand Up @@ -318,6 +341,10 @@ export default {
if (!message) {
return;
}
if (message.length > CHAT_MESSAGE_MAX_LENGTH) {
toastChatError({ code: "too_long", max: CHAT_MESSAGE_MAX_LENGTH });
return;
}
this.$emit(
"sendMessage",
message,
Expand Down
5 changes: 4 additions & 1 deletion components/chat/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@ export default {
if (this.previousMessage.__channel !== this.message.__channel) {
return false;
}
return this.message.from.steam_id === this.previousMessage.from.steam_id;
return (
String(this.message.from?.steam_id) ===
String(this.previousMessage.from?.steam_id)
);
},
isCloseTogether() {
if (!this.isSameSender || !this.previousMessage) {
Expand Down
3 changes: 3 additions & 0 deletions constants/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Must match the api's ChatService.MAX_MESSAGE_LENGTH. Both sides count
// .length (UTF-16 code units) of the trimmed text.
export const CHAT_MESSAGE_MAX_LENGTH = 2000;
2 changes: 2 additions & 0 deletions i18n/locales/ar_SA.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "اضغط Ctrl+Enter للإرسال إلى {channel}",
"everyone_hint": "كلا الفريقين — ويظهر أيضًا في دردشة اللعبة",
"team_hint": "تشكيلتك فقط — ولا يظهر في دردشة اللعبة",
"message_too_long": "الحد الأقصى لطول الرسالة هو {max} حرف.",
"send_failed": "تعذّر إرسال رسالتك",
"direct": {
"message": "رسالة",
"empty": "لا رسائل بعد. ألقِ التحية."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/da_DK.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter for at sende til {channel}",
"everyone_hint": "Begge hold — vises også i spillets chat",
"team_hint": "Kun din opstilling — vises ikke i spillets chat",
"message_too_long": "Beskeder kan højst være på {max} tegn.",
"send_failed": "Din besked kunne ikke sendes",
"direct": {
"message": "Besked",
"empty": "Ingen beskeder endnu. Sig hej."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/de_DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Strg+Enter, um an {channel} zu senden",
"everyone_hint": "Beide Teams — wird auch im Spielchat angezeigt",
"team_hint": "Nur dein Lineup — wird nicht im Spielchat angezeigt",
"message_too_long": "Nachrichten dürfen höchstens {max} Zeichen lang sein.",
"send_failed": "Deine Nachricht konnte nicht gesendet werden",
"direct": {
"message": "Nachricht",
"empty": "Noch keine Nachrichten. Sag Hallo."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter to send to {channel}",
"everyone_hint": "Both teams — also shown in game chat",
"team_hint": "Your lineup only — not shown in game chat",
"message_too_long": "Messages can be up to {max} characters.",
"send_failed": "Failed to send message",
"direct": {
"message": "Message",
"empty": "No messages yet. Say hello."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/es_ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Intro para enviar a {channel}",
"everyone_hint": "Ambos equipos: también se muestra en el chat del juego",
"team_hint": "Solo tu alineación: no se muestra en el chat del juego",
"message_too_long": "Los mensajes pueden tener un máximo de {max} caracteres.",
"send_failed": "No se ha podido enviar tu mensaje",
"direct": {
"message": "Mensaje",
"empty": "Aún no hay mensajes. Saluda."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Entrée pour envoyer à {channel}",
"everyone_hint": "Les deux équipes — également affiché dans le chat du jeu",
"team_hint": "Votre composition uniquement — non affiché dans le chat du jeu",
"message_too_long": "Les messages peuvent contenir jusqu'à {max} caractères.",
"send_failed": "Votre message n'a pas pu être envoyé",
"direct": {
"message": "Message",
"empty": "Aucun message pour l'instant. Dites bonjour."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/it_IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Invio per inviare a {channel}",
"everyone_hint": "Entrambe le squadre — mostrato anche nella chat di gioco",
"team_hint": "Solo la tua formazione — non mostrato nella chat di gioco",
"message_too_long": "I messaggi possono contenere al massimo {max} caratteri.",
"send_failed": "Non è stato possibile inviare il tuo messaggio",
"direct": {
"message": "Messaggio",
"empty": "Ancora nessun messaggio. Fatti sentire."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/ja_JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enterで {channel} に送信",
"everyone_hint": "両チーム — ゲーム内チャットにも表示されます",
"team_hint": "自分のラインナップのみ — ゲーム内チャットには表示されません",
"message_too_long": "メッセージは最大{max}文字までです。",
"send_failed": "メッセージを送信できませんでした",
"direct": {
"message": "メッセージ",
"empty": "まだメッセージはありません。あいさつしてみましょう。"
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/ko_KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter로 {channel}에 보내기",
"everyone_hint": "양 팀 — 게임 내 채팅에도 표시됩니다",
"team_hint": "내 라인업만 — 게임 내 채팅에는 표시되지 않습니다",
"message_too_long": "메시지는 최대 {max}자까지 입력할 수 있습니다.",
"send_failed": "메시지를 보내지 못했습니다",
"direct": {
"message": "메시지",
"empty": "아직 메시지가 없습니다. 인사를 건네보세요."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/pl_PL.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter, aby wysłać do {channel}",
"everyone_hint": "Obie drużyny — pokazywane także na czacie w grze",
"team_hint": "Tylko twój skład — nie jest pokazywane na czacie w grze",
"message_too_long": "Wiadomość może mieć maksymalnie {max} znaków.",
"send_failed": "Nie udało się wysłać wiadomości",
"direct": {
"message": "Wiadomość",
"empty": "Brak wiadomości. Przywitaj się."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/pt_BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter para enviar para {channel}",
"everyone_hint": "Ambas as equipes — também mostrado no chat do jogo",
"team_hint": "Apenas sua equipe — não mostrado no chat do jogo",
"message_too_long": "As mensagens podem ter no máximo {max} caracteres.",
"send_failed": "Não foi possível enviar sua mensagem",
"direct": {
"message": "Mensagem",
"empty": "Nenhuma mensagem ainda. Diga Olá."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/ru_RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter, чтобы отправить в {channel}",
"everyone_hint": "Обе команды — также отображается в игровом чате",
"team_hint": "Только ваш состав — в игровом чате не отображается",
"message_too_long": "Сообщение может содержать не более {max} символов.",
"send_failed": "Не удалось отправить сообщение",
"direct": {
"message": "Сообщение",
"empty": "Сообщений пока нет. Поздоровайтесь."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/sv_SE.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter för att skicka till {channel}",
"everyone_hint": "Båda lagen — visas även i spelets chatt",
"team_hint": "Bara din laguppställning — visas inte i spelets chatt",
"message_too_long": "Meddelanden får vara högst {max} tecken långa.",
"send_failed": "Ditt meddelande kunde inte skickas",
"direct": {
"message": "Meddelande",
"empty": "Inga meddelanden än. Säg hej."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/tr_TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "{channel} kanalına göndermek için Ctrl+Enter",
"everyone_hint": "Her iki takım — oyun içi sohbette de görünür",
"team_hint": "Yalnızca kendi kadron — oyun içi sohbette görünmez",
"message_too_long": "Mesajlar en fazla {max} karakter olabilir.",
"send_failed": "Mesajın gönderilemedi",
"direct": {
"message": "Mesaj",
"empty": "Henüz mesaj yok. Merhaba de."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/uk_UA.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "Ctrl+Enter, щоб надіслати в {channel}",
"everyone_hint": "Обидві команди — також показується в ігровому чаті",
"team_hint": "Лише ваш склад — в ігровому чаті не показується",
"message_too_long": "Повідомлення може містити не більше {max} символів.",
"send_failed": "Не вдалося надіслати повідомлення",
"direct": {
"message": "Повідомлення",
"empty": "Повідомлень поки немає. Привітайтеся."
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/zh_Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "按 Ctrl+Enter 发送到 {channel}",
"everyone_hint": "两支战队 — 也会显示在游戏内聊天",
"team_hint": "仅限你的阵容 — 不会显示在游戏内聊天",
"message_too_long": "消息最多 {max} 个字符。",
"send_failed": "消息发送失败",
"direct": {
"message": "消息",
"empty": "还没有消息,先打个招呼吧。"
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/zh_Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,8 @@
"send_other_hint": "按 Ctrl+Enter 傳送到 {channel}",
"everyone_hint": "兩支隊伍 — 也會顯示在遊戲內聊天",
"team_hint": "僅限你的名單 — 不會顯示在遊戲內聊天",
"message_too_long": "訊息最多 {max} 個字元。",
"send_failed": "訊息傳送失敗",
"direct": {
"message": "訊息",
"empty": "還沒有訊息,先打個招呼吧。"
Expand Down
120 changes: 120 additions & 0 deletions tests/components/ChatInput.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { flushPromises } from "@vue/test-utils";
import { mountSuspended } from "@nuxt/test-utils/runtime";
import ChatInput from "~/components/chat/ChatInput.vue";
import { CHAT_MESSAGE_MAX_LENGTH } from "~/constants/chat";

const { toast } = vi.hoisted(() => ({ toast: vi.fn() }));

vi.mock("@/components/ui/toast", async (importOriginal) => ({
...(await importOriginal<typeof import("@/components/ui/toast")>()),
toast,
}));

type Wrapper = Awaited<ReturnType<typeof mountSuspended>>;

async function type(wrapper: Wrapper, text: string) {
await wrapper.get("textarea").setValue(text);
await flushPromises();
}

async function submit(wrapper: Wrapper) {
await wrapper.get("form").trigger("submit");
await flushPromises();
}

const textarea = (wrapper: Wrapper) =>
wrapper.get("textarea").element as HTMLTextAreaElement;

const remaining = (wrapper: Wrapper) => wrapper.find(".tabular-nums");

describe("ChatInput", () => {
beforeEach(() => {
toast.mockClear();
});

it("sends the trimmed text and clears the box", async () => {
const wrapper = await mountSuspended(ChatInput);

await type(wrapper, " gg wp ");
await submit(wrapper);

expect(wrapper.emitted("sendMessage")).toEqual([["gg wp", undefined]]);
expect(textarea(wrapper).value).toBe("");
expect(toast).not.toHaveBeenCalled();
});

it("sends a message of exactly the limit", async () => {
const wrapper = await mountSuspended(ChatInput);
const message = "a".repeat(CHAT_MESSAGE_MAX_LENGTH);

await type(wrapper, message);
await submit(wrapper);

expect(wrapper.emitted("sendMessage")).toEqual([[message, undefined]]);
expect(toast).not.toHaveBeenCalled();
});

it("measures the limit after trimming, like the api", async () => {
const wrapper = await mountSuspended(ChatInput);
const message = "a".repeat(CHAT_MESSAGE_MAX_LENGTH);

await type(wrapper, ` ${message}\n `);
await submit(wrapper);

expect(wrapper.emitted("sendMessage")).toEqual([[message, undefined]]);
});

it("refuses an over-length message and keeps the text", async () => {
const wrapper = await mountSuspended(ChatInput);
const message = "a".repeat(CHAT_MESSAGE_MAX_LENGTH + 1);

await type(wrapper, message);
await submit(wrapper);

expect(wrapper.emitted("sendMessage")).toBeUndefined();
expect(textarea(wrapper).value).toBe(message);
expect(toast).toHaveBeenCalledTimes(1);
expect(toast).toHaveBeenCalledWith({
title: "Failed to send message",
description: "Messages can be up to 2000 characters.",
variant: "destructive",
});
});

it("refuses an over-length message sent with enter", async () => {
const wrapper = await mountSuspended(ChatInput);
const message = "a".repeat(CHAT_MESSAGE_MAX_LENGTH + 1);

await type(wrapper, message);
await wrapper.get("textarea").trigger("keydown", { key: "Enter" });
await flushPromises();

expect(wrapper.emitted("sendMessage")).toBeUndefined();
expect(textarea(wrapper).value).toBe(message);
expect(toast).toHaveBeenCalledTimes(1);
});

it("counts down only near the limit", async () => {
const wrapper = await mountSuspended(ChatInput);

await type(wrapper, "a".repeat(CHAT_MESSAGE_MAX_LENGTH - 201));
expect(remaining(wrapper).exists()).toBe(false);

await type(wrapper, "a".repeat(CHAT_MESSAGE_MAX_LENGTH - 200));
expect(remaining(wrapper).text()).toBe("200");
expect(remaining(wrapper).classes()).not.toContain("text-destructive");

await type(wrapper, "a".repeat(CHAT_MESSAGE_MAX_LENGTH + 5));
expect(remaining(wrapper).text()).toBe("-5");
expect(remaining(wrapper).classes()).toContain("text-destructive");
});

it("counts what will be sent, not surrounding whitespace", async () => {
const wrapper = await mountSuspended(ChatInput);

await type(wrapper, ` ${"a".repeat(CHAT_MESSAGE_MAX_LENGTH - 200)}\n `);

expect(remaining(wrapper).text()).toBe("200");
});
});
Loading
Loading