aboutsummaryrefslogtreecommitdiffstats
path: root/chat/templates/room.html
diff options
context:
space:
mode:
Diffstat (limited to 'chat/templates/room.html')
-rw-r--r--chat/templates/room.html451
1 files changed, 451 insertions, 0 deletions
diff --git a/chat/templates/room.html b/chat/templates/room.html
new file mode 100644
index 0000000..de76f67
--- /dev/null
+++ b/chat/templates/room.html
@@ -0,0 +1,451 @@
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="utf-8"/>
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <meta name="theme-color" content="#c9b8ff">
7 <title>{{ room_name }} — dreamchat</title>
8 <style>
9 :root {
10 --grad-start: #c9b8ff;
11 --grad-end: #f9b8e8;
12 --accent: #8b6cef;
13 --bubble-in-bg: #f0ebff;
14 --bubble-in-text: #2d2040;
15 --surface: #ffffff;
16 --text-main: #2d2040;
17 --text-muted: #8c7faa;
18 --radius-bubble: 18px;
19 --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
20 }
21
22 * { box-sizing: border-box; margin: 0; padding: 0; }
23
24 body {
25 height: 100dvh;
26 display: flex;
27 flex-direction: column;
28 background: linear-gradient(160deg, #ede7ff 0%, #fce4f6 100%);
29 font-family: var(--font);
30 overflow: hidden;
31 }
32
33 /* ---- Header ---- */
34 .room-header {
35 display: flex;
36 align-items: center;
37 gap: 12px;
38 padding: 10px 16px;
39 padding-top: calc(10px + env(safe-area-inset-top));
40 background: rgba(255, 255, 255, 0.75);
41 backdrop-filter: blur(12px);
42 -webkit-backdrop-filter: blur(12px);
43 border-bottom: 1px solid rgba(139, 108, 239, 0.12);
44 flex-shrink: 0;
45 }
46
47 .back-btn {
48 display: flex;
49 align-items: center;
50 justify-content: center;
51 width: 32px;
52 height: 32px;
53 border-radius: 50%;
54 color: var(--accent);
55 text-decoration: none;
56 font-size: 1.2rem;
57 font-weight: 600;
58 transition: background 0.15s;
59 flex-shrink: 0;
60 }
61
62 .back-btn:hover { background: #f0ebff; }
63
64 .header-center {
65 flex: 1;
66 min-width: 0;
67 display: flex;
68 flex-direction: column;
69 gap: 2px;
70 }
71
72 .room-title {
73 font-size: 1.05rem;
74 font-weight: 700;
75 background: linear-gradient(135deg, var(--accent), #e06aba);
76 -webkit-background-clip: text;
77 -webkit-text-fill-color: transparent;
78 background-clip: text;
79 white-space: nowrap;
80 overflow: hidden;
81 text-overflow: ellipsis;
82 }
83
84 .username-row { display: flex; align-items: center; }
85
86 .username-chip {
87 display: flex;
88 align-items: center;
89 gap: 4px;
90 background: none;
91 border: none;
92 padding: 0;
93 font-size: 0.78rem;
94 font-family: var(--font);
95 color: var(--text-muted);
96 cursor: pointer;
97 transition: color 0.15s;
98 }
99
100 .username-chip:hover { color: var(--accent); }
101 .edit-icon { font-size: 0.7rem; }
102
103 .username-edit {
104 font-size: 0.78rem;
105 font-family: var(--font);
106 color: var(--text-main);
107 border: none;
108 border-bottom: 1.5px solid var(--accent);
109 outline: none;
110 background: transparent;
111 width: 130px;
112 padding: 1px 2px;
113 }
114
115 .disconnect-btn {
116 display: flex;
117 align-items: center;
118 justify-content: center;
119 width: 32px;
120 height: 32px;
121 background: none;
122 border: 1.5px solid #d0c5f0;
123 color: var(--text-muted);
124 border-radius: 50%;
125 font-size: 0.85rem;
126 cursor: pointer;
127 flex-shrink: 0;
128 transition: background 0.15s, border-color 0.15s;
129 }
130
131 .disconnect-btn:hover { background: #f0ebff; border-color: var(--accent); }
132
133 /* ---- Message area ---- */
134 .chat-log {
135 flex: 1;
136 overflow-y: auto;
137 -webkit-overflow-scrolling: touch;
138 padding: 16px 12px;
139 display: flex;
140 flex-direction: column;
141 gap: 6px;
142 }
143
144 /* ---- Bubbles ---- */
145 .bubble-row {
146 display: flex;
147 width: 100%;
148 }
149
150 .bubble-row.sent { justify-content: flex-end; }
151 .bubble-row.recv { justify-content: flex-start; }
152
153 .bubble-wrapper {
154 display: flex;
155 flex-direction: column;
156 max-width: 72%;
157 gap: 3px;
158 }
159
160 .bubble-row.sent .bubble-wrapper { align-items: flex-end; }
161 .bubble-row.recv .bubble-wrapper { align-items: flex-start; }
162
163 .bubble {
164 padding: 10px 15px;
165 border-radius: var(--radius-bubble);
166 font-size: 0.97rem;
167 line-height: 1.45;
168 word-wrap: break-word;
169 animation: pop-in 0.15s ease-out;
170 }
171
172 .sent .bubble {
173 background: linear-gradient(135deg, var(--accent), #b06add);
174 color: #ffffff;
175 border-bottom-right-radius: 4px;
176 }
177
178 .recv .bubble {
179 background: var(--bubble-in-bg);
180 color: var(--bubble-in-text);
181 border-bottom-left-radius: 4px;
182 box-shadow: 0 2px 8px rgba(139, 108, 239, 0.1);
183 }
184
185 .bubble-sender {
186 font-size: 0.75rem;
187 font-weight: 600;
188 color: var(--accent);
189 padding: 0 4px;
190 }
191
192 .bubble-time {
193 font-size: 0.7rem;
194 color: var(--text-muted);
195 padding: 0 4px;
196 }
197
198 @keyframes pop-in {
199 from { opacity: 0; transform: scale(0.9) translateY(4px); }
200 to { opacity: 1; transform: scale(1) translateY(0); }
201 }
202
203 /* ---- Input bar ---- */
204 .input-bar {
205 display: flex;
206 align-items: center;
207 gap: 10px;
208 padding: 12px 16px;
209 padding-bottom: calc(12px + env(safe-area-inset-bottom));
210 background: rgba(255, 255, 255, 0.85);
211 backdrop-filter: blur(12px);
212 -webkit-backdrop-filter: blur(12px);
213 border-top: 1px solid rgba(139, 108, 239, 0.12);
214 flex-shrink: 0;
215 }
216
217 .input-bar input {
218 flex: 1;
219 min-width: 0;
220 border: 2px solid #e8e0ff;
221 border-radius: 50px;
222 padding: 11px 18px;
223 font-size: 1rem;
224 font-family: var(--font);
225 color: var(--text-main);
226 background: #fff;
227 outline: none;
228 transition: border-color 0.2s;
229 }
230
231 .input-bar input:focus { border-color: var(--accent); }
232 .input-bar input::placeholder { color: var(--text-muted); }
233
234 .send-btn {
235 display: flex;
236 align-items: center;
237 justify-content: center;
238 width: 44px;
239 height: 44px;
240 background: linear-gradient(135deg, var(--accent), #b06add);
241 color: #fff;
242 border: none;
243 border-radius: 50%;
244 font-size: 1rem;
245 cursor: pointer;
246 flex-shrink: 0;
247 transition: opacity 0.15s, transform 0.1s;
248 }
249
250 .send-btn:hover { opacity: 0.9; }
251 .send-btn:active { transform: scale(0.93); }
252
253 /* ---- System messages ---- */
254 .system-msg {
255 display: flex;
256 align-items: center;
257 gap: 10px;
258 font-size: 0.72rem;
259 color: var(--text-muted);
260 padding: 4px 0;
261 animation: pop-in 0.2s ease-out;
262 }
263
264 .system-msg::before,
265 .system-msg::after {
266 content: '';
267 flex: 1;
268 height: 1px;
269 background: rgba(139, 108, 239, 0.2);
270 }
271 </style>
272</head>
273<body>
274 <header class="room-header">
275 <a href="/" class="back-btn" aria-label="Back to rooms">←</a>
276 <div class="header-center">
277 <span class="room-title">{{ room_name }}</span>
278 <div class="username-row">
279 <button id="username-display" class="username-chip" aria-label="Edit username">
280 <span id="username-text"></span>
281 <span class="edit-icon">✏️</span>
282 </button>
283 <input id="username-input" class="username-edit"
284 aria-label="Your username" hidden
285 maxlength="30" autocomplete="off" spellcheck="false">
286 </div>
287 </div>
288 <button id="chat-message-disconnect" class="disconnect-btn" hidden aria-label="Disconnect">✕</button>
289 </header>
290
291 <main id="chat-log" class="chat-log" aria-live="polite" aria-label="Chat messages"></main>
292
293 <footer class="input-bar">
294 <input id="chat-message-input" type="text" placeholder="Message…"
295 aria-label="Message"
296 autocomplete="off" autocapitalize="sentences">
297 <button id="chat-message-submit" class="send-btn" aria-label="Send">➤</button>
298 </footer>
299
300 {{ room_name|json_script:"room-name" }}
301 <script>
302 const ADJECTIVES = ['Dreamy','Cosmic','Fuzzy','Glowing','Misty','Silky','Starry','Velvet','Cozy','Dewy','Wispy','Breezy'];
303 const ANIMALS = ['Fox','Moth','Owl','Bear','Deer','Lark','Lynx','Mink','Swan','Wolf','Wren','Panda'];
304
305 function generateUsername() {
306 const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
307 const animal = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
308 return adj + animal;
309 }
310
311 let username = localStorage.getItem('dreamchat_username') || generateUsername();
312 localStorage.setItem('dreamchat_username', username);
313 document.getElementById('username-text').textContent = username;
314
315 document.getElementById('username-display').onclick = function() {
316 const input = document.getElementById('username-input');
317 input.value = username;
318 input.removeAttribute('hidden');
319 this.setAttribute('hidden', '');
320 input.focus();
321 input.select();
322 };
323
324 function commitUsername() {
325 const input = document.getElementById('username-input');
326 const newName = input.value.trim();
327 if (newName && newName !== username) {
328 username = newName;
329 localStorage.setItem('dreamchat_username', username);
330 document.getElementById('username-text').textContent = username;
331 if (chatSocket.readyState === WebSocket.OPEN) {
332 chatSocket.send(JSON.stringify({ type: 'rename', username }));
333 }
334 }
335 input.setAttribute('hidden', '');
336 document.getElementById('username-display').removeAttribute('hidden');
337 }
338
339 document.getElementById('username-input').onblur = commitUsername;
340 document.getElementById('username-input').onkeyup = function(event) {
341 if (event.key === 'Enter') { this.blur(); }
342 if (event.key === 'Escape') {
343 this.setAttribute('hidden', '');
344 document.getElementById('username-display').removeAttribute('hidden');
345 }
346 };
347
348 function formatTime(date) {
349 return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
350 }
351
352 const roomName = JSON.parse(document.getElementById('room-name').textContent);
353 const chatLog = document.getElementById('chat-log');
354 const pendingEchoes = new Set();
355
356 function appendSystemMessage(text) {
357 const el = document.createElement('div');
358 el.className = 'system-msg';
359 el.textContent = text;
360 chatLog.appendChild(el);
361 chatLog.scrollTop = chatLog.scrollHeight;
362 }
363
364 function appendBubble(text, direction, sender, time) {
365 const row = document.createElement('div');
366 row.className = 'bubble-row ' + direction;
367
368 const wrapper = document.createElement('div');
369 wrapper.className = 'bubble-wrapper';
370
371 if (direction === 'recv') {
372 const nameEl = document.createElement('div');
373 nameEl.className = 'bubble-sender';
374 nameEl.textContent = sender;
375 wrapper.appendChild(nameEl);
376 }
377
378 const bubble = document.createElement('div');
379 bubble.className = 'bubble';
380 bubble.textContent = text;
381 wrapper.appendChild(bubble);
382
383 const timeEl = document.createElement('div');
384 timeEl.className = 'bubble-time';
385 timeEl.textContent = time;
386 wrapper.appendChild(timeEl);
387
388 row.appendChild(wrapper);
389 chatLog.appendChild(row);
390 chatLog.scrollTop = chatLog.scrollHeight;
391 }
392
393 const chatSocket = new WebSocket(
394 'ws://'
395 + window.location.host
396 + '/ws/chat/'
397 + roomName
398 + '/'
399 + '?token=123'
400 );
401
402 chatSocket.onopen = function() {
403 document.querySelector('#chat-message-disconnect').removeAttribute('hidden');
404 chatSocket.send(JSON.stringify({ type: 'join', username }));
405 };
406
407 chatSocket.onmessage = function(e) {
408 const data = JSON.parse(e.data);
409
410 if (data.type === 'system') {
411 appendSystemMessage(data.text);
412 return;
413 }
414
415 const key = data.username + '|' + data.message;
416 if (pendingEchoes.has(key)) {
417 pendingEchoes.delete(key);
418 return;
419 }
420 appendBubble(data.message, 'recv', data.username, formatTime(new Date()));
421 };
422
423 chatSocket.onclose = function() {
424 console.error('Chat socket closed unexpectedly');
425 document.querySelector('#chat-message-disconnect').setAttribute('hidden', '');
426 };
427
428 document.querySelector('#chat-message-input').focus();
429 document.querySelector('#chat-message-input').onkeyup = function(event) {
430 if (event.key === 'Enter') {
431 document.querySelector('#chat-message-submit').click();
432 }
433 };
434
435 document.querySelector('#chat-message-submit').onclick = function() {
436 const messageInputDom = document.querySelector('#chat-message-input');
437 const message = messageInputDom.value.trim();
438 if (!message) return;
439 const time = formatTime(new Date());
440 pendingEchoes.add(username + '|' + message);
441 appendBubble(message, 'sent', username, time);
442 chatSocket.send(JSON.stringify({ message, username }));
443 messageInputDom.value = '';
444 };
445
446 document.querySelector('#chat-message-disconnect').onclick = function() {
447 chatSocket.close();
448 };
449 </script>
450</body>
451</html>