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
47 changes: 35 additions & 12 deletions components/chat/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,23 @@ const fieldName = `chat-message-${Math.random().toString(36).slice(2, 10)}`;
: 'flex items-center gap-2 p-2'
"
>
<Input
<Textarea
ref="inputRef"
rows="1"
:placeholder="activePlaceholder"
v-bind="componentField"
type="text"
autocomplete="off"
:name="fieldName"
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
data-form-type="other"
:class="
:class="[
'min-h-0 resize-none py-1.5 leading-snug',
variant === 'global'
? 'flex-1 transition-all duration-200 focus:scale-[1.02]'
: 'flex-1 resize-none border-0 shadow-none focus-visible:ring-0'
"
? 'flex-1 transition-[border-color,box-shadow] duration-200'
: 'flex-1 border-0 shadow-none focus-visible:ring-0',
]"
@keydown.enter="onEnter"
/>
<Button
Expand Down Expand Up @@ -153,6 +154,7 @@ import { FormControl, FormField, FormItem } from "~/components/ui/form";
import * as z from "zod";
import { useForm } from "vee-validate";
import { toTypedSchema } from "~/utilities/vee-validate-zod";
import { chatEnterAction } from "~/utilities/chatInputKeys";

export interface ChatInputChannel {
value: string;
Expand Down Expand Up @@ -184,6 +186,14 @@ export default {
},
},
emits: ["sendMessage", "update:destination"],
watch: {
// Not @input: v-bind="componentField" already binds one, and a second
// would replace vee-validate's. Watching the value also covers a paste and
// the reset after sending.
"form.values.message"() {
void this.$nextTick(() => this.growToFit());
},
},
data() {
return {
sending: false,
Expand Down Expand Up @@ -279,20 +289,33 @@ export default {
// the other room without moving them -- the common case is a single team
// callout in the middle of talking to everyone.
onEnter(event: KeyboardEvent) {
if (!event.metaKey && !event.ctrlKey) {
const action = chatEnterAction(event);

if (action === "newline") {
return;
}

if (!this.otherChannelValue) {
// The box is a textarea now, so nothing submits the form on its own.
event.preventDefault();

this.sendMessage(
action === "send-other" ? this.otherChannelValue : undefined,
);
},
// One line until the message needs more, then up to five.
growToFit() {
const field = this.$refs.inputRef?.$el ?? this.$refs.inputRef;

if (!field) {
return;
}

event.preventDefault();
this.sendMessage(this.otherChannelValue);
field.style.height = "auto";
field.style.height = `${Math.min(field.scrollHeight, 120)}px`;
},
sendMessage(destination?: string) {
const { message } = this.form.values;
if (!message || message?.length === 0) {
const message = this.form.values.message?.trim();
if (!message) {
return;
}
this.$emit(
Expand Down
2 changes: 1 addition & 1 deletion components/chat/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import PlayerDisplay from "~/components/PlayerDisplay.vue";
{{ $t("chat.team_tag") }}
</span>
</div>
<p class="text-[11px] leading-snug break-words">
<p class="text-[11px] leading-snug break-words whitespace-pre-wrap">
{{ message.message }}
</p>
</div>
Expand Down
39 changes: 39 additions & 0 deletions tests/utilities/chatInputKeys.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { chatEnterAction } from "~/utilities/chatInputKeys";

const key = (overrides: Partial<KeyboardEvent> = {}) =>
({
shiftKey: false,
metaKey: false,
ctrlKey: false,
altKey: false,
isComposing: false,
...overrides,
}) as KeyboardEvent;

describe("chatEnterAction", () => {
it("sends on a bare enter", () => {
expect(chatEnterAction(key())).toBe("send");
});

it("makes a new line on shift+enter", () => {
expect(chatEnterAction(key({ shiftKey: true }))).toBe("newline");
});

it("sends to the other channel on ctrl or cmd enter", () => {
expect(chatEnterAction(key({ ctrlKey: true }))).toBe("send-other");
expect(chatEnterAction(key({ metaKey: true }))).toBe("send-other");
});

it("leaves an in-progress IME composition alone", () => {
// enter confirms the candidate in a Japanese or Korean IME; sending there
// would cut the word off mid-composition
expect(chatEnterAction(key({ isComposing: true }))).toBe("newline");
});

it("prefers the other channel over a new line when both modifiers are held", () => {
expect(chatEnterAction(key({ shiftKey: true, ctrlKey: true }))).toBe(
"send-other",
);
});
});
24 changes: 24 additions & 0 deletions utilities/chatInputKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type ChatEnterAction = "send" | "send-other" | "newline";

/**
* What Enter means in the chat box.
*
* Enter sends, Shift+Enter breaks the line, and Ctrl/Cmd+Enter sends to the
* other room (match chat from team chat and back), which wins over a line break
* when both are held.
*
* A keypress that is confirming an IME candidate is never a send: in a Japanese
* or Korean composition Enter picks the word, and sending there would cut it
* off mid-word.
*/
export function chatEnterAction(event: KeyboardEvent): ChatEnterAction {
if (event.metaKey || event.ctrlKey) {
return "send-other";
}

if (event.isComposing) {
return "newline";
}

return event.shiftKey ? "newline" : "send";
}
Loading