mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
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.0, maximum-scale=1.0, user-scalable=no" />
6 <title>Messages - Lazurite</title>
7 <link rel="preconnect" href="https://fonts.googleapis.com" />
8 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9 <link href="https://fonts.googleapis.com/css2?family=Lora:wght@400;500;600;700&display=swap" rel="stylesheet" />
10 <link
11 href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"
12 rel="stylesheet" />
13 <link rel="stylesheet" href="styles.css" />
14 <style>
15 .messages-container {
16 padding-bottom: 88px;
17 }
18
19 /* Tabs */
20 .msg-tabs {
21 display: flex;
22 border-bottom: 1px solid var(--border);
23 background-color: var(--bg);
24 }
25
26 .msg-tab {
27 flex: 1;
28 padding: 14px;
29 text-align: center;
30 font-weight: 600;
31 font-size: 15px;
32 color: var(--text-secondary);
33 cursor: pointer;
34 border-bottom: 2px solid transparent;
35 transition: all 0.2s ease;
36 background: none;
37 border-top: none;
38 border-left: none;
39 border-right: none;
40 }
41
42 .msg-tab:hover {
43 background-color: var(--surface);
44 color: var(--text-primary);
45 }
46
47 .msg-tab.active {
48 color: var(--text-primary);
49 border-bottom-color: var(--accent-primary);
50 }
51
52 .msg-tab-badge {
53 display: inline-flex;
54 align-items: center;
55 justify-content: center;
56 min-width: 18px;
57 height: 18px;
58 padding: 0 5px;
59 border-radius: 9px;
60 background-color: var(--accent-error);
61 color: white;
62 font-size: 10px;
63 font-weight: 700;
64 margin-left: 6px;
65 }
66
67 /* Conversation Item */
68 .convo-item {
69 display: flex;
70 align-items: center;
71 gap: 12px;
72 padding: 14px 16px;
73 border-bottom: 1px solid var(--border);
74 cursor: pointer;
75 transition: background-color 0.2s ease;
76 position: relative;
77 }
78
79 .convo-item:hover {
80 background-color: var(--surface);
81 }
82
83 .convo-item.unread {
84 background-color: var(--surface);
85 }
86
87 .convo-avatar {
88 position: relative;
89 flex-shrink: 0;
90 }
91
92 .convo-avatar .avatar {
93 width: 48px;
94 height: 48px;
95 }
96
97 .convo-unread-dot {
98 position: absolute;
99 top: 0;
100 right: 0;
101 width: 12px;
102 height: 12px;
103 border-radius: 50%;
104 background-color: var(--accent-primary);
105 border: 2px solid var(--bg);
106 }
107
108 .convo-info {
109 flex: 1;
110 min-width: 0;
111 }
112
113 .convo-header {
114 display: flex;
115 align-items: baseline;
116 justify-content: space-between;
117 gap: 8px;
118 margin-bottom: 2px;
119 }
120
121 .convo-name {
122 font-weight: 600;
123 font-size: 15px;
124 color: var(--text-primary);
125 white-space: nowrap;
126 overflow: hidden;
127 text-overflow: ellipsis;
128 }
129
130 .convo-time {
131 font-size: 12px;
132 color: var(--text-muted);
133 flex-shrink: 0;
134 }
135
136 .convo-last-message {
137 font-size: 14px;
138 color: var(--text-secondary);
139 white-space: nowrap;
140 overflow: hidden;
141 text-overflow: ellipsis;
142 }
143
144 .convo-last-message.unread {
145 color: var(--text-primary);
146 font-weight: 500;
147 }
148
149 .convo-muted-icon {
150 width: 14px;
151 height: 14px;
152 color: var(--text-muted);
153 flex-shrink: 0;
154 }
155
156 /* New Message FAB */
157 .fab-new-message {
158 position: fixed;
159 bottom: 100px;
160 right: calc(50% - 207px + 16px);
161 width: 56px;
162 height: 56px;
163 border-radius: 50%;
164 background-color: var(--accent-primary);
165 color: white;
166 border: none;
167 cursor: pointer;
168 display: flex;
169 align-items: center;
170 justify-content: center;
171 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
172 transition: all 0.2s ease;
173 z-index: 50;
174 }
175
176 .fab-new-message:hover {
177 transform: scale(1.05);
178 box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
179 }
180
181 .fab-new-message svg {
182 width: 24px;
183 height: 24px;
184 }
185
186 /* Message Thread View */
187 .thread-header {
188 display: flex;
189 align-items: center;
190 gap: 12px;
191 padding: 12px 16px;
192 border-bottom: 1px solid var(--border);
193 background-color: var(--bg);
194 position: sticky;
195 top: 0;
196 z-index: 50;
197 }
198
199 .thread-back {
200 background: none;
201 border: none;
202 color: var(--text-secondary);
203 cursor: pointer;
204 display: flex;
205 align-items: center;
206 }
207
208 .thread-back svg {
209 width: 24px;
210 height: 24px;
211 }
212
213 .thread-info {
214 flex: 1;
215 }
216
217 .thread-name {
218 font-weight: 600;
219 font-size: 16px;
220 color: var(--text-primary);
221 }
222
223 .thread-handle {
224 font-size: 13px;
225 color: var(--text-secondary);
226 }
227
228 .thread-overflow {
229 background: none;
230 border: none;
231 color: var(--text-secondary);
232 cursor: pointer;
233 padding: 8px;
234 }
235
236 .thread-overflow svg {
237 width: 20px;
238 height: 20px;
239 }
240
241 /* Chat Bubbles */
242 .chat-area {
243 padding: 16px;
244 display: flex;
245 flex-direction: column;
246 gap: 8px;
247 padding-bottom: 80px;
248 }
249
250 .chat-bubble-row {
251 display: flex;
252 gap: 8px;
253 max-width: 80%;
254 }
255
256 .chat-bubble-row.sent {
257 align-self: flex-end;
258 flex-direction: row-reverse;
259 }
260
261 .chat-bubble-row.received {
262 align-self: flex-start;
263 }
264
265 .chat-bubble {
266 padding: 10px 14px;
267 border-radius: 18px;
268 font-size: 15px;
269 line-height: 1.4;
270 position: relative;
271 }
272
273 .chat-bubble.sent {
274 background-color: var(--accent-primary);
275 color: white;
276 border-bottom-right-radius: 4px;
277 }
278
279 .chat-bubble.received {
280 background-color: var(--surface);
281 color: var(--text-primary);
282 border: 1px solid var(--border);
283 border-bottom-left-radius: 4px;
284 }
285
286 .chat-time {
287 font-size: 11px;
288 color: var(--text-muted);
289 margin-top: 4px;
290 padding: 0 4px;
291 }
292
293 .chat-time.sent {
294 text-align: right;
295 }
296
297 .chat-date-divider {
298 text-align: center;
299 padding: 12px 0;
300 font-size: 12px;
301 color: var(--text-muted);
302 font-weight: 500;
303 }
304
305 /* Message Input */
306 .msg-input-bar {
307 position: fixed;
308 bottom: 0;
309 left: 50%;
310 transform: translateX(-50%);
311 width: 100%;
312 max-width: 414px;
313 display: flex;
314 align-items: center;
315 gap: 8px;
316 padding: 12px 16px;
317 background-color: var(--bg);
318 border-top: 1px solid var(--border);
319 z-index: 100;
320 }
321
322 .msg-input {
323 flex: 1;
324 padding: 10px 16px;
325 border: 1px solid var(--border);
326 border-radius: 9999px;
327 background-color: var(--surface);
328 color: var(--text-primary);
329 font-size: 15px;
330 font-family: var(--font-body);
331 outline: none;
332 transition: border-color 0.2s ease;
333 }
334
335 .msg-input:focus {
336 border-color: var(--accent-primary);
337 }
338
339 .msg-input::placeholder {
340 color: var(--text-muted);
341 }
342
343 .msg-send-btn {
344 width: 40px;
345 height: 40px;
346 border-radius: 50%;
347 border: none;
348 background-color: var(--accent-primary);
349 color: white;
350 cursor: pointer;
351 display: flex;
352 align-items: center;
353 justify-content: center;
354 transition: background-color 0.2s ease;
355 flex-shrink: 0;
356 }
357
358 .msg-send-btn:hover {
359 background-color: var(--accent-primary-hover);
360 }
361
362 .msg-send-btn svg {
363 width: 18px;
364 height: 18px;
365 }
366
367 /* Nav badge */
368 .nav-item {
369 position: relative;
370 }
371
372 .nav-item-badge {
373 position: absolute;
374 top: 2px;
375 right: 8px;
376 min-width: 18px;
377 height: 18px;
378 padding: 0 5px;
379 border-radius: 9px;
380 background-color: var(--accent-error);
381 color: white;
382 font-size: 10px;
383 font-weight: 700;
384 display: flex;
385 align-items: center;
386 justify-content: center;
387 }
388
389 /* View toggle */
390 .view-toggle {
391 display: none;
392 }
393 .view-toggle.active {
394 display: block;
395 }
396 </style>
397 </head>
398 <body>
399 <div class="mobile-container">
400 <!-- ==================== -->
401 <!-- CONVERSATION LIST VIEW -->
402 <!-- ==================== -->
403 <div class="view-toggle active" id="list-view">
404 <!-- Header -->
405 <header class="header">
406 <h1 class="header-title">Messages</h1>
407 <button class="header-action" onclick="toggleView()">Open Thread</button>
408 </header>
409
410 <!-- Tabs -->
411 <div class="msg-tabs">
412 <button class="msg-tab active">Primary</button>
413 <button class="msg-tab">Requests <span class="msg-tab-badge">2</span></button>
414 </div>
415
416 <div class="messages-container">
417 <!-- Unread conversation -->
418 <div class="convo-item unread">
419 <div class="convo-avatar">
420 <div class="avatar">AS</div>
421 <div class="convo-unread-dot"></div>
422 </div>
423 <div class="convo-info">
424 <div class="convo-header">
425 <span class="convo-name">Alice Smith</span>
426 <span class="convo-time">12m</span>
427 </div>
428 <div class="convo-last-message unread">
429 Hey! Did you see the new federation update? It's really exciting.
430 </div>
431 </div>
432 </div>
433
434 <!-- Unread conversation -->
435 <div class="convo-item unread">
436 <div class="convo-avatar">
437 <div class="avatar">BJ</div>
438 <div class="convo-unread-dot"></div>
439 </div>
440 <div class="convo-info">
441 <div class="convo-header">
442 <span class="convo-name">Bob Johnson</span>
443 <span class="convo-time">2h</span>
444 </div>
445 <div class="convo-last-message unread">Would love to collaborate on the AT Protocol project</div>
446 </div>
447 </div>
448
449 <!-- Read conversation -->
450 <div class="convo-item">
451 <div class="convo-avatar">
452 <div class="avatar">CW</div>
453 </div>
454 <div class="convo-info">
455 <div class="convo-header">
456 <span class="convo-name">Carol White</span>
457 <span class="convo-time">1d</span>
458 </div>
459 <div class="convo-last-message">Thanks for sharing that article! I'll check it out this weekend.</div>
460 </div>
461 </div>
462
463 <!-- Muted conversation -->
464 <div class="convo-item">
465 <div class="convo-avatar">
466 <div class="avatar">DM</div>
467 </div>
468 <div class="convo-info">
469 <div class="convo-header">
470 <span class="convo-name">David Miller</span>
471 <span class="convo-time">3d</span>
472 </div>
473 <div class="convo-last-message">Sounds good, let's catch up next week then!</div>
474 </div>
475 <svg
476 class="convo-muted-icon"
477 viewBox="0 0 24 24"
478 fill="none"
479 stroke="currentColor"
480 stroke-width="2"
481 stroke-linecap="round"
482 stroke-linejoin="round">
483 <path d="M11 5L6 9H2v6h4l5 4V5z" />
484 <line x1="23" y1="9" x2="17" y2="15" />
485 <line x1="17" y1="9" x2="23" y2="15" />
486 </svg>
487 </div>
488
489 <!-- Old conversation -->
490 <div class="convo-item">
491 <div class="convo-avatar">
492 <div class="avatar">EL</div>
493 </div>
494 <div class="convo-info">
495 <div class="convo-header">
496 <span class="convo-name">Eva Lee</span>
497 <span class="convo-time">1w</span>
498 </div>
499 <div class="convo-last-message">Great chatting with you at the meetup!</div>
500 </div>
501 </div>
502 </div>
503
504 <!-- New Message FAB -->
505 <button class="fab-new-message" title="New message">
506 <svg
507 viewBox="0 0 24 24"
508 fill="none"
509 stroke="currentColor"
510 stroke-width="2"
511 stroke-linecap="round"
512 stroke-linejoin="round">
513 <line x1="22" y1="2" x2="11" y2="13" />
514 <polygon points="22 2 15 22 11 13 2 9 22 2" />
515 </svg>
516 </button>
517
518 <!-- Bottom Navigation -->
519 <nav class="nav-bar">
520 <a href="home.html" class="nav-item">
521 <svg
522 viewBox="0 0 24 24"
523 fill="none"
524 stroke="currentColor"
525 stroke-width="2"
526 stroke-linecap="round"
527 stroke-linejoin="round">
528 <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
529 <polyline points="9 22 9 12 15 12 15 22" />
530 </svg>
531 <span>Home</span>
532 </a>
533
534 <a href="search.html" class="nav-item">
535 <svg
536 viewBox="0 0 24 24"
537 fill="none"
538 stroke="currentColor"
539 stroke-width="2"
540 stroke-linecap="round"
541 stroke-linejoin="round">
542 <circle cx="11" cy="11" r="8" />
543 <line x1="21" y1="21" x2="16.65" y2="16.65" />
544 </svg>
545 <span>Search</span>
546 </a>
547
548 <a href="notifications.html" class="nav-item">
549 <svg
550 viewBox="0 0 24 24"
551 fill="none"
552 stroke="currentColor"
553 stroke-width="2"
554 stroke-linecap="round"
555 stroke-linejoin="round">
556 <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
557 <path d="M13.73 21a2 2 0 0 1-3.46 0" />
558 </svg>
559 <span>Alerts</span>
560 </a>
561
562 <a href="messages.html" class="nav-item active">
563 <svg
564 viewBox="0 0 24 24"
565 fill="none"
566 stroke="currentColor"
567 stroke-width="2"
568 stroke-linecap="round"
569 stroke-linejoin="round">
570 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
571 </svg>
572 <span class="nav-item-badge">2</span>
573 <span>Chat</span>
574 </a>
575
576 <a href="profile.html" class="nav-item">
577 <svg
578 viewBox="0 0 24 24"
579 fill="none"
580 stroke="currentColor"
581 stroke-width="2"
582 stroke-linecap="round"
583 stroke-linejoin="round">
584 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
585 <circle cx="12" cy="7" r="4" />
586 </svg>
587 <span>Profile</span>
588 </a>
589 </nav>
590 </div>
591
592 <!-- ==================== -->
593 <!-- MESSAGE THREAD VIEW -->
594 <!-- ==================== -->
595 <div class="view-toggle" id="thread-view">
596 <!-- Thread Header -->
597 <div class="thread-header">
598 <button class="thread-back" onclick="toggleView()">
599 <svg
600 viewBox="0 0 24 24"
601 fill="none"
602 stroke="currentColor"
603 stroke-width="2"
604 stroke-linecap="round"
605 stroke-linejoin="round">
606 <polyline points="15 18 9 12 15 6" />
607 </svg>
608 </button>
609 <div class="thread-info">
610 <div class="thread-name">Alice Smith</div>
611 <div class="thread-handle">@alice.bsky.social</div>
612 </div>
613 <button class="thread-overflow">
614 <svg
615 viewBox="0 0 24 24"
616 fill="none"
617 stroke="currentColor"
618 stroke-width="2"
619 stroke-linecap="round"
620 stroke-linejoin="round">
621 <circle cx="12" cy="12" r="1" />
622 <circle cx="19" cy="12" r="1" />
623 <circle cx="5" cy="12" r="1" />
624 </svg>
625 </button>
626 </div>
627
628 <!-- Chat Area -->
629 <div class="chat-area">
630 <div class="chat-date-divider">Today</div>
631
632 <div class="chat-bubble-row received">
633 <div class="chat-bubble received">Hey! Did you see the new federation update? It's really exciting.</div>
634 </div>
635 <div class="chat-time">10:23 AM</div>
636
637 <div class="chat-bubble-row sent">
638 <div class="chat-bubble sent">
639 Yes! I was just reading through the announcement. The self-hosting guide looks much improved.
640 </div>
641 </div>
642 <div class="chat-time sent">10:25 AM</div>
643
644 <div class="chat-bubble-row received">
645 <div class="chat-bubble received">
646 Right? I'm thinking of setting up my own PDS this weekend. Want to try it together?
647 </div>
648 </div>
649 <div class="chat-time">10:27 AM</div>
650
651 <div class="chat-bubble-row sent">
652 <div class="chat-bubble sent">
653 That sounds great! I've been meaning to do that for a while. Let me know when you're free.
654 </div>
655 </div>
656 <div class="chat-time sent">10:30 AM</div>
657
658 <div class="chat-bubble-row received">
659 <div class="chat-bubble received">
660 Saturday afternoon works for me. We could do a video call and set them up at the same time.
661 </div>
662 </div>
663 <div class="chat-time">10:32 AM</div>
664 </div>
665
666 <!-- Message Input -->
667 <div class="msg-input-bar">
668 <input class="msg-input" type="text" placeholder="Type a message..." />
669 <button class="msg-send-btn">
670 <svg
671 viewBox="0 0 24 24"
672 fill="none"
673 stroke="currentColor"
674 stroke-width="2"
675 stroke-linecap="round"
676 stroke-linejoin="round">
677 <line x1="22" y1="2" x2="11" y2="13" />
678 <polygon points="22 2 15 22 11 13 2 9 22 2" />
679 </svg>
680 </button>
681 </div>
682 </div>
683 </div>
684
685 <script>
686 if (localStorage.getItem("theme")) {
687 const t = localStorage.getItem("theme");
688 if (t !== "light") document.documentElement.setAttribute("data-theme", t);
689 }
690
691 function toggleView() {
692 document.getElementById("list-view").classList.toggle("active");
693 document.getElementById("thread-view").classList.toggle("active");
694 }
695
696 document.querySelectorAll(".msg-tab").forEach((tab) => {
697 tab.addEventListener("click", () => {
698 document.querySelectorAll(".msg-tab").forEach((t) => t.classList.remove("active"));
699 tab.classList.add("active");
700 });
701 });
702 </script>
703 </body>
704</html>