|
20 | 20 | <div ref="messages" @scroll="onScroll" class="messages grow">
|
21 | 21 | <div class="grow"><!-- Spacer --></div>
|
22 | 22 | <transition-group name="message">
|
23 |
| - <div class="message" v-for="(msg, index) in chatMessagePast" :key="index"> |
24 |
| - <div class="from">{{ msg.from.name }}</div> |
25 |
| - <div class="text"> |
26 |
| - <ProcessedText :text="msg.text" @link-click="e => $emit('link-click', e)" /> |
27 |
| - </div> |
28 |
| - </div> |
29 |
| - <div |
30 |
| - class="message recent" |
| 23 | + <ChatMsg |
| 24 | + :msg="msg" |
| 25 | + v-for="(msg, index) in chatMessagePast" |
| 26 | + :key="index" |
| 27 | + @link-click="emit('link-click', $event)" |
| 28 | + /> |
| 29 | + <ChatMsg |
| 30 | + :msg="msg" |
| 31 | + recent |
31 | 32 | v-for="(msg, index) in chatMessageRecent"
|
32 | 33 | :key="chatMessagePast.length + index"
|
33 |
| - > |
34 |
| - <!-- FIXME: reduce duplicated code by moving this to another component, preferably in this same file. --> |
35 |
| - <div class="from">{{ msg.from.name }}</div> |
36 |
| - <div class="text"><ProcessedText :text="msg.text" /></div> |
37 |
| - </div> |
| 34 | + @link-click="emit('link-click', $event)" |
| 35 | + /> |
38 | 36 | </transition-group>
|
39 | 37 | </div>
|
40 | 38 | <div v-if="!stickToBottom" class="to-bottom">
|
|
74 | 72 | </div>
|
75 | 73 | </template>
|
76 | 74 |
|
77 |
| -<script lang="ts"> |
78 |
| -import ProcessedText from "@/components/ProcessedText.vue"; |
79 |
| -import { defineComponent, onUpdated, ref, Ref, nextTick, onMounted, onUnmounted } from "vue"; |
| 75 | +<script lang="ts" setup> |
| 76 | +import { onUpdated, ref, Ref, nextTick, onMounted, onUnmounted } from "vue"; |
80 | 77 | import type { ChatMessage } from "ott-common/models/types";
|
81 | 78 | import { useConnection } from "@/plugins/connection";
|
82 | 79 | import { useRoomApi } from "@/util/roomapi";
|
83 | 80 | import { ServerMessageChat } from "ott-common/models/messages";
|
84 | 81 | import { useRoomKeyboardShortcuts } from "@/util/keyboard-shortcuts";
|
85 | 82 | import { useSfx } from "@/plugins/sfx";
|
| 83 | +import ChatMsg from "./ChatMsg.vue"; |
86 | 84 |
|
87 | 85 | const MSG_SHOW_TIMEOUT = 20000;
|
88 | 86 |
|
89 |
| -const Chat = defineComponent({ |
90 |
| - name: "Chat", |
91 |
| - components: { |
92 |
| - ProcessedText, |
93 |
| - }, |
94 |
| - emits: ["link-click"], |
95 |
| - setup() { |
96 |
| - const connection = useConnection(); |
97 |
| - const roomapi = useRoomApi(connection); |
98 |
| -
|
99 |
| - const inputValue = ref(""); |
100 |
| - const stickToBottom = ref(true); |
101 |
| - /** |
102 |
| - * When chat is activated, all messages are shown. and the |
103 |
| - * user can scroll through message history, type in chat, etc. |
104 |
| - * When chat is NOT activated, when messages are received, |
105 |
| - * they appear and fade away after `MSG_SHOW_TIMEOUT` ms. |
106 |
| - */ |
107 |
| - const activated = ref(false); |
108 |
| - const deactivateOnBlur = ref(false); |
109 |
| - /** |
110 |
| - * All past chat messages. They are are no longer |
111 |
| - * shown when deactivated. |
112 |
| - */ |
113 |
| - const chatMessagePast: Ref<ChatMessage[]> = ref([]); |
114 |
| - /** |
115 |
| - * All recent chat messages that are currently shown when deactivated. |
116 |
| - * They will fade away after `MSG_SHOW_TIMEOUT` ms, and moved into `chatMessagePast`. |
117 |
| - */ |
118 |
| - const chatMessageRecent: Ref<ChatMessage[]> = ref([]); |
119 |
| - const messages = ref(); |
120 |
| - const chatInput: Ref<HTMLInputElement | undefined> = ref(); |
121 |
| -
|
122 |
| - const shortcuts = useRoomKeyboardShortcuts(); |
123 |
| - onMounted(() => { |
124 |
| - connection.addMessageHandler("chat", onChatReceived); |
125 |
| - if (shortcuts) { |
126 |
| - shortcuts.bind({ code: "KeyT" }, () => setActivated(true, false)); |
127 |
| - } else { |
128 |
| - console.warn("No keyboard shortcuts available"); |
129 |
| - } |
130 |
| - }); |
131 |
| -
|
132 |
| - onUnmounted(() => { |
133 |
| - connection.removeMessageHandler("chat", onChatReceived); |
134 |
| - }); |
135 |
| -
|
136 |
| - function focusChatInput() { |
137 |
| - chatInput.value?.focus(); |
138 |
| - } |
| 87 | +const emit = defineEmits(["link-click"]); |
| 88 | +
|
| 89 | +const connection = useConnection(); |
| 90 | +const roomapi = useRoomApi(connection); |
| 91 | +
|
| 92 | +const inputValue = ref(""); |
| 93 | +const stickToBottom = ref(true); |
| 94 | +/** |
| 95 | + * When chat is activated, all messages are shown. and the |
| 96 | + * user can scroll through message history, type in chat, etc. |
| 97 | + * When chat is NOT activated, when messages are received, |
| 98 | + * they appear and fade away after `MSG_SHOW_TIMEOUT` ms. |
| 99 | + */ |
| 100 | +const activated = ref(false); |
| 101 | +const deactivateOnBlur = ref(false); |
| 102 | +/** |
| 103 | + * All past chat messages. They are are no longer |
| 104 | + * shown when deactivated. |
| 105 | + */ |
| 106 | +const chatMessagePast: Ref<ChatMessage[]> = ref([]); |
| 107 | +/** |
| 108 | + * All recent chat messages that are currently shown when deactivated. |
| 109 | + * They will fade away after `MSG_SHOW_TIMEOUT` ms, and moved into `chatMessagePast`. |
| 110 | + */ |
| 111 | +const chatMessageRecent: Ref<ChatMessage[]> = ref([]); |
| 112 | +const messages = ref(); |
| 113 | +const chatInput: Ref<HTMLInputElement | undefined> = ref(); |
| 114 | +
|
| 115 | +const shortcuts = useRoomKeyboardShortcuts(); |
| 116 | +onMounted(() => { |
| 117 | + connection.addMessageHandler("chat", onChatReceived); |
| 118 | + if (shortcuts) { |
| 119 | + shortcuts.bind({ code: "KeyT" }, () => setActivated(true, false)); |
| 120 | + } else { |
| 121 | + console.warn("No keyboard shortcuts available"); |
| 122 | + } |
| 123 | +}); |
139 | 124 |
|
140 |
| - function isActivated(): boolean { |
141 |
| - return activated.value; |
142 |
| - } |
| 125 | +onUnmounted(() => { |
| 126 | + connection.removeMessageHandler("chat", onChatReceived); |
| 127 | +}); |
143 | 128 |
|
144 |
| - async function setActivated(value: boolean, manual = false): Promise<void> { |
145 |
| - activated.value = value; |
146 |
| - if (value) { |
147 |
| - if (manual) { |
148 |
| - deactivateOnBlur.value = false; |
149 |
| - } else { |
150 |
| - deactivateOnBlur.value = true; |
151 |
| - } |
152 |
| - await nextTick(); |
153 |
| - focusChatInput(); |
154 |
| - } else { |
155 |
| - chatInput.value?.blur(); |
156 |
| - forceToBottom(); |
157 |
| - } |
158 |
| - } |
| 129 | +function focusChatInput() { |
| 130 | + chatInput.value?.focus(); |
| 131 | +} |
159 | 132 |
|
160 |
| - const sfx = useSfx(); |
161 |
| - function onChatReceived(msg: ServerMessageChat): void { |
162 |
| - chatMessageRecent.value.push(msg); |
163 |
| - setTimeout(expireChatMessage, MSG_SHOW_TIMEOUT); |
164 |
| - nextTick(enforceStickToBottom); |
165 |
| - sfx.play("pop"); |
| 133 | +async function setActivated(value: boolean, manual = false): Promise<void> { |
| 134 | + activated.value = value; |
| 135 | + if (value) { |
| 136 | + if (manual) { |
| 137 | + deactivateOnBlur.value = false; |
| 138 | + } else { |
| 139 | + deactivateOnBlur.value = true; |
166 | 140 | }
|
| 141 | + await nextTick(); |
| 142 | + focusChatInput(); |
| 143 | + } else { |
| 144 | + chatInput.value?.blur(); |
| 145 | + forceToBottom(); |
| 146 | + } |
| 147 | +} |
167 | 148 |
|
168 |
| - function expireChatMessage() { |
169 |
| - chatMessagePast.value.push(chatMessageRecent.value.splice(0, 1)[0]); |
170 |
| - } |
| 149 | +const sfx = useSfx(); |
| 150 | +function onChatReceived(msg: ServerMessageChat): void { |
| 151 | + chatMessageRecent.value.push(msg); |
| 152 | + setTimeout(expireChatMessage, MSG_SHOW_TIMEOUT); |
| 153 | + nextTick(enforceStickToBottom); |
| 154 | + sfx.play("pop"); |
| 155 | +} |
171 | 156 |
|
172 |
| - /** |
173 |
| - * Performs the necessary actions to enact the stickToBottom behavior. |
174 |
| - */ |
175 |
| - function enforceStickToBottom() { |
176 |
| - const div = messages.value as HTMLDivElement; |
177 |
| - if (stickToBottom.value) { |
178 |
| - div.scrollTop = div.scrollHeight; |
179 |
| - } |
180 |
| - } |
| 157 | +function expireChatMessage() { |
| 158 | + chatMessagePast.value.push(chatMessageRecent.value.splice(0, 1)[0]); |
| 159 | +} |
181 | 160 |
|
182 |
| - function onInputKeyDown(e: KeyboardEvent): void { |
183 |
| - if (e.key === "Enter") { |
184 |
| - e.preventDefault(); |
185 |
| - if (inputValue.value.trim() !== "") { |
186 |
| - roomapi.chat(inputValue.value); |
187 |
| - } |
188 |
| - inputValue.value = ""; |
189 |
| - stickToBottom.value = true; |
190 |
| - setActivated(false); |
191 |
| - } else if (e.key === "Escape") { |
192 |
| - e.preventDefault(); |
193 |
| - setActivated(false); |
194 |
| - } |
195 |
| - } |
| 161 | +/** |
| 162 | + * Performs the necessary actions to enact the stickToBottom behavior. |
| 163 | + */ |
| 164 | +function enforceStickToBottom() { |
| 165 | + const div = messages.value as HTMLDivElement; |
| 166 | + if (stickToBottom.value) { |
| 167 | + div.scrollTop = div.scrollHeight; |
| 168 | + } |
| 169 | +} |
196 | 170 |
|
197 |
| - function onScroll() { |
198 |
| - const div = messages.value as HTMLDivElement; |
199 |
| - const distToBottom = div.scrollHeight - div.clientHeight - div.scrollTop; |
200 |
| - stickToBottom.value = distToBottom === 0; |
| 171 | +function onInputKeyDown(e: KeyboardEvent): void { |
| 172 | + if (e.key === "Enter") { |
| 173 | + e.preventDefault(); |
| 174 | + if (inputValue.value.trim() !== "") { |
| 175 | + roomapi.chat(inputValue.value); |
201 | 176 | }
|
| 177 | + inputValue.value = ""; |
| 178 | + stickToBottom.value = true; |
| 179 | + setActivated(false); |
| 180 | + } else if (e.key === "Escape") { |
| 181 | + e.preventDefault(); |
| 182 | + setActivated(false); |
| 183 | + } |
| 184 | +} |
202 | 185 |
|
203 |
| - function forceToBottom() { |
204 |
| - stickToBottom.value = true; |
205 |
| - enforceStickToBottom(); |
206 |
| - } |
| 186 | +function onScroll() { |
| 187 | + const div = messages.value as HTMLDivElement; |
| 188 | + const distToBottom = div.scrollHeight - div.clientHeight - div.scrollTop; |
| 189 | + stickToBottom.value = distToBottom === 0; |
| 190 | +} |
207 | 191 |
|
208 |
| - onUpdated(enforceStickToBottom); |
209 |
| -
|
210 |
| - return { |
211 |
| - inputValue, |
212 |
| - stickToBottom, |
213 |
| - activated, |
214 |
| - chatMessagePast, |
215 |
| - chatMessageRecent, |
216 |
| - deactivateOnBlur, |
217 |
| -
|
218 |
| - onInputKeyDown, |
219 |
| - onScroll, |
220 |
| - focusChatInput, |
221 |
| - isActivated, |
222 |
| - setActivated, |
223 |
| - onChatReceived, |
224 |
| - enforceStickToBottom, |
225 |
| - forceToBottom, |
226 |
| -
|
227 |
| - messages, |
228 |
| - chatInput, |
229 |
| - }; |
230 |
| - }, |
231 |
| -}); |
| 192 | +function forceToBottom() { |
| 193 | + stickToBottom.value = true; |
| 194 | + enforceStickToBottom(); |
| 195 | +} |
232 | 196 |
|
233 |
| -export default Chat; |
| 197 | +onUpdated(enforceStickToBottom); |
234 | 198 | </script>
|
235 | 199 |
|
236 | 200 | <style lang="scss" scoped>
|
@@ -300,39 +264,6 @@ $chat-message-bg: $background-color;
|
300 | 264 | align-items: baseline;
|
301 | 265 | }
|
302 | 266 |
|
303 |
| -.message { |
304 |
| - margin: 2px 0; |
305 |
| - padding: 4px; |
306 |
| - opacity: 0; |
307 |
| - transition: all 1s ease; |
308 |
| -
|
309 |
| - &:first-child { |
310 |
| - margin-top: auto; |
311 |
| - } |
312 |
| -
|
313 |
| - &.recent { |
314 |
| - opacity: 1; |
315 |
| - background: rgba(var(--v-theme-background), $alpha: 0.6); |
316 |
| - } |
317 |
| -
|
318 |
| - .from, |
319 |
| - .text { |
320 |
| - display: inline; |
321 |
| - margin: 3px 5px; |
322 |
| - word-wrap: break-word; |
323 |
| - overflow-wrap: anywhere; |
324 |
| - } |
325 |
| -
|
326 |
| - .from { |
327 |
| - font-weight: bold; |
328 |
| - margin-left: 0; |
329 |
| - } |
330 |
| -
|
331 |
| - @media screen and (max-width: $sm-max) { |
332 |
| - font-size: 0.8em; |
333 |
| - } |
334 |
| -} |
335 |
| -
|
336 | 267 | .manual-activate {
|
337 | 268 | display: flex;
|
338 | 269 | align-self: flex-end;
|
|
0 commit comments