mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: phase 3 plan & designs

+1967
+515
docs/designs/compose.html
··· 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>Compose - 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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.2.2/dist/fonts/geist-sans/style.css"> 11 + <link rel="stylesheet" href="styles.css"> 12 + <style> 13 + .compose-header { 14 + display: flex; 15 + align-items: center; 16 + justify-content: space-between; 17 + padding: 12px 16px; 18 + border-bottom: 1px solid var(--border); 19 + background-color: var(--bg); 20 + position: sticky; 21 + top: 0; 22 + z-index: 50; 23 + } 24 + 25 + .compose-header-cancel { 26 + background: none; 27 + border: none; 28 + color: var(--text-secondary); 29 + font-size: 15px; 30 + font-weight: 500; 31 + cursor: pointer; 32 + } 33 + 34 + .compose-header-title { 35 + font-size: 18px; 36 + font-weight: 600; 37 + color: var(--text-primary); 38 + font-family: var(--font-heading); 39 + } 40 + 41 + .compose-header-post { 42 + padding: 8px 20px; 43 + border-radius: 9999px; 44 + border: none; 45 + background-color: var(--accent-primary); 46 + color: white; 47 + font-weight: 600; 48 + font-size: 14px; 49 + cursor: pointer; 50 + transition: background-color 0.2s ease; 51 + } 52 + 53 + .compose-header-post:hover { 54 + background-color: var(--accent-primary-hover); 55 + } 56 + 57 + .compose-header-post:disabled { 58 + opacity: 0.5; 59 + cursor: not-allowed; 60 + } 61 + 62 + .compose-body { 63 + padding: 16px; 64 + display: flex; 65 + gap: 12px; 66 + min-height: 200px; 67 + } 68 + 69 + .compose-textarea { 70 + flex: 1; 71 + border: none; 72 + background: transparent; 73 + color: var(--text-primary); 74 + font-size: 16px; 75 + font-family: var(--font-body); 76 + resize: none; 77 + outline: none; 78 + min-height: 160px; 79 + line-height: 1.5; 80 + } 81 + 82 + .compose-textarea::placeholder { 83 + color: var(--text-muted); 84 + } 85 + 86 + /* Reply Context */ 87 + .reply-context { 88 + padding: 12px 16px; 89 + border-bottom: 1px solid var(--border); 90 + display: flex; 91 + align-items: center; 92 + gap: 8px; 93 + background-color: var(--surface); 94 + } 95 + 96 + .reply-context-icon { 97 + width: 16px; 98 + height: 16px; 99 + color: var(--text-muted); 100 + } 101 + 102 + .reply-context-text { 103 + font-size: 13px; 104 + color: var(--text-secondary); 105 + } 106 + 107 + .reply-context-handle { 108 + color: var(--accent-primary); 109 + font-weight: 500; 110 + } 111 + 112 + /* Media Attachments */ 113 + .compose-media { 114 + padding: 0 16px 16px; 115 + padding-left: 68px; 116 + } 117 + 118 + .media-grid { 119 + display: grid; 120 + grid-template-columns: repeat(2, 1fr); 121 + gap: 8px; 122 + } 123 + 124 + .media-item { 125 + position: relative; 126 + border-radius: 12px; 127 + overflow: hidden; 128 + background: linear-gradient(135deg, var(--surface) 0%, var(--surface-variant) 100%); 129 + aspect-ratio: 4 / 3; 130 + display: flex; 131 + align-items: center; 132 + justify-content: center; 133 + color: var(--text-muted); 134 + font-size: 13px; 135 + } 136 + 137 + .media-item-remove { 138 + position: absolute; 139 + top: 6px; 140 + right: 6px; 141 + width: 24px; 142 + height: 24px; 143 + border-radius: 50%; 144 + background-color: rgba(0, 0, 0, 0.6); 145 + border: none; 146 + color: white; 147 + cursor: pointer; 148 + display: flex; 149 + align-items: center; 150 + justify-content: center; 151 + } 152 + 153 + .media-item-remove svg { 154 + width: 14px; 155 + height: 14px; 156 + } 157 + 158 + .media-item-alt { 159 + position: absolute; 160 + bottom: 6px; 161 + left: 6px; 162 + padding: 2px 8px; 163 + border-radius: 4px; 164 + background-color: rgba(0, 0, 0, 0.6); 165 + color: white; 166 + font-size: 11px; 167 + font-weight: 600; 168 + cursor: pointer; 169 + } 170 + 171 + .media-item-alt.has-alt { 172 + background-color: var(--accent-primary); 173 + } 174 + 175 + /* Toolbar */ 176 + .compose-toolbar { 177 + display: flex; 178 + align-items: center; 179 + justify-content: space-between; 180 + padding: 12px 16px; 181 + border-top: 1px solid var(--border); 182 + position: sticky; 183 + bottom: 0; 184 + background-color: var(--bg); 185 + } 186 + 187 + .toolbar-actions { 188 + display: flex; 189 + gap: 4px; 190 + } 191 + 192 + .toolbar-btn { 193 + width: 40px; 194 + height: 40px; 195 + border-radius: 50%; 196 + border: none; 197 + background: transparent; 198 + color: var(--accent-primary); 199 + cursor: pointer; 200 + display: flex; 201 + align-items: center; 202 + justify-content: center; 203 + transition: background-color 0.2s ease; 204 + } 205 + 206 + .toolbar-btn:hover { 207 + background-color: var(--surface); 208 + } 209 + 210 + .toolbar-btn svg { 211 + width: 22px; 212 + height: 22px; 213 + } 214 + 215 + .toolbar-btn.disabled { 216 + color: var(--text-muted); 217 + cursor: not-allowed; 218 + } 219 + 220 + /* Character Counter */ 221 + .char-counter { 222 + display: flex; 223 + align-items: center; 224 + gap: 8px; 225 + } 226 + 227 + .char-counter-ring { 228 + width: 28px; 229 + height: 28px; 230 + position: relative; 231 + } 232 + 233 + .char-counter-ring svg { 234 + width: 28px; 235 + height: 28px; 236 + transform: rotate(-90deg); 237 + } 238 + 239 + .char-counter-ring circle { 240 + fill: none; 241 + stroke-width: 2.5; 242 + } 243 + 244 + .char-counter-bg { 245 + stroke: var(--surface-variant); 246 + } 247 + 248 + .char-counter-fill { 249 + stroke: var(--accent-primary); 250 + stroke-dasharray: 75.4; 251 + stroke-dashoffset: 30; 252 + stroke-linecap: round; 253 + transition: stroke-dashoffset 0.2s ease, stroke 0.2s ease; 254 + } 255 + 256 + .char-counter-fill.warning { 257 + stroke: var(--accent-warning); 258 + } 259 + 260 + .char-counter-fill.danger { 261 + stroke: var(--accent-error); 262 + } 263 + 264 + .char-counter-text { 265 + font-size: 13px; 266 + color: var(--text-muted); 267 + font-variant-numeric: tabular-nums; 268 + } 269 + 270 + /* Drafts */ 271 + .drafts-divider { 272 + height: 8px; 273 + background-color: var(--surface); 274 + border-top: 1px solid var(--border); 275 + border-bottom: 1px solid var(--border); 276 + } 277 + 278 + .drafts-header { 279 + display: flex; 280 + align-items: center; 281 + justify-content: space-between; 282 + padding: 16px; 283 + } 284 + 285 + .drafts-title { 286 + font-size: 16px; 287 + font-weight: 600; 288 + color: var(--text-primary); 289 + } 290 + 291 + .drafts-count { 292 + font-size: 13px; 293 + color: var(--text-muted); 294 + } 295 + 296 + .draft-item { 297 + padding: 12px 16px; 298 + border-bottom: 1px solid var(--border); 299 + cursor: pointer; 300 + transition: background-color 0.2s ease; 301 + } 302 + 303 + .draft-item:hover { 304 + background-color: var(--surface); 305 + } 306 + 307 + .draft-item-text { 308 + font-size: 15px; 309 + color: var(--text-primary); 310 + line-height: 1.4; 311 + display: -webkit-box; 312 + -webkit-line-clamp: 2; 313 + -webkit-box-orient: vertical; 314 + overflow: hidden; 315 + } 316 + 317 + .draft-item-meta { 318 + display: flex; 319 + align-items: center; 320 + gap: 8px; 321 + margin-top: 6px; 322 + font-size: 12px; 323 + color: var(--text-muted); 324 + } 325 + 326 + .draft-item-badge { 327 + padding: 2px 6px; 328 + border-radius: 4px; 329 + background-color: var(--surface-variant); 330 + font-size: 11px; 331 + font-weight: 600; 332 + color: var(--text-secondary); 333 + } 334 + 335 + .draft-item-badge.scheduled { 336 + background-color: var(--accent-primary); 337 + color: white; 338 + } 339 + 340 + /* Schedule Pill */ 341 + .schedule-pill { 342 + display: inline-flex; 343 + align-items: center; 344 + gap: 6px; 345 + padding: 6px 12px; 346 + border-radius: 9999px; 347 + background-color: var(--surface); 348 + border: 1px solid var(--border); 349 + font-size: 13px; 350 + color: var(--text-secondary); 351 + margin-left: 68px; 352 + margin-bottom: 12px; 353 + cursor: pointer; 354 + transition: all 0.2s ease; 355 + } 356 + 357 + .schedule-pill:hover { 358 + border-color: var(--accent-primary); 359 + color: var(--accent-primary); 360 + } 361 + 362 + .schedule-pill svg { 363 + width: 14px; 364 + height: 14px; 365 + } 366 + 367 + .schedule-pill.active { 368 + background-color: var(--accent-primary); 369 + border-color: var(--accent-primary); 370 + color: white; 371 + } 372 + </style> 373 + </head> 374 + <body> 375 + <div class="mobile-container"> 376 + 377 + <!-- Compose Header --> 378 + <div class="compose-header"> 379 + <button class="compose-header-cancel">Cancel</button> 380 + <span class="compose-header-title">New Post</span> 381 + <button class="compose-header-post">Post</button> 382 + </div> 383 + 384 + <!-- Reply Context (shown when replying) --> 385 + <div class="reply-context"> 386 + <svg class="reply-context-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 387 + <polyline points="9 14 4 9 9 4"/> 388 + <path d="M20 20v-7a4 4 0 0 0-4-4H4"/> 389 + </svg> 390 + <span class="reply-context-text">Replying to <span class="reply-context-handle">@alice.bsky.social</span></span> 391 + </div> 392 + 393 + <!-- Compose Body --> 394 + <div class="compose-body"> 395 + <div class="avatar avatar-sm">JD</div> 396 + <textarea class="compose-textarea" placeholder="What's on your mind?" rows="6">Excited to share my latest project built with the AT Protocol! Check it out and let me know what you think.</textarea> 397 + </div> 398 + 399 + <!-- Schedule Pill --> 400 + <div class="schedule-pill"> 401 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 402 + <circle cx="12" cy="12" r="10"/> 403 + <polyline points="12 6 12 12 16 14"/> 404 + </svg> 405 + Schedule for later 406 + </div> 407 + 408 + <!-- Media Attachments --> 409 + <div class="compose-media"> 410 + <div class="media-grid"> 411 + <div class="media-item"> 412 + [Photo 1] 413 + <button class="media-item-remove"> 414 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 415 + <line x1="18" y1="6" x2="6" y2="18"/> 416 + <line x1="6" y1="6" x2="18" y2="18"/> 417 + </svg> 418 + </button> 419 + <span class="media-item-alt has-alt">ALT</span> 420 + </div> 421 + <div class="media-item"> 422 + [Photo 2] 423 + <button class="media-item-remove"> 424 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 425 + <line x1="18" y1="6" x2="6" y2="18"/> 426 + <line x1="6" y1="6" x2="18" y2="18"/> 427 + </svg> 428 + </button> 429 + <span class="media-item-alt">ALT</span> 430 + </div> 431 + </div> 432 + </div> 433 + 434 + <!-- Toolbar --> 435 + <div class="compose-toolbar"> 436 + <div class="toolbar-actions"> 437 + <!-- Image --> 438 + <button class="toolbar-btn" title="Add image"> 439 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 440 + <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/> 441 + <circle cx="8.5" cy="8.5" r="1.5"/> 442 + <polyline points="21 15 16 10 5 21"/> 443 + </svg> 444 + </button> 445 + <!-- Drafts --> 446 + <button class="toolbar-btn" title="Drafts"> 447 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 448 + <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> 449 + <polyline points="14 2 14 8 20 8"/> 450 + <line x1="16" y1="13" x2="8" y2="13"/> 451 + <line x1="16" y1="17" x2="8" y2="17"/> 452 + </svg> 453 + </button> 454 + <!-- Schedule --> 455 + <button class="toolbar-btn" title="Schedule"> 456 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 457 + <circle cx="12" cy="12" r="10"/> 458 + <polyline points="12 6 12 12 16 14"/> 459 + </svg> 460 + </button> 461 + </div> 462 + 463 + <div class="char-counter"> 464 + <span class="char-counter-text">192</span> 465 + <div class="char-counter-ring"> 466 + <svg viewBox="0 0 28 28"> 467 + <circle class="char-counter-bg" cx="14" cy="14" r="12"/> 468 + <circle class="char-counter-fill" cx="14" cy="14" r="12"/> 469 + </svg> 470 + </div> 471 + </div> 472 + </div> 473 + 474 + <!-- Drafts Section (shown when drafts toolbar button is tapped) --> 475 + <div class="drafts-divider"></div> 476 + 477 + <div class="drafts-header"> 478 + <span class="drafts-title">Drafts</span> 479 + <span class="drafts-count">3 drafts</span> 480 + </div> 481 + 482 + <div class="draft-item"> 483 + <div class="draft-item-text">Working on a thread about decentralised identity and why it matters for the open web...</div> 484 + <div class="draft-item-meta"> 485 + <span>2 hours ago</span> 486 + <span class="draft-item-badge">Draft</span> 487 + </div> 488 + </div> 489 + 490 + <div class="draft-item"> 491 + <div class="draft-item-text">Hot take: the best developer experience is the one you don't notice</div> 492 + <div class="draft-item-meta"> 493 + <span>Yesterday</span> 494 + <span class="draft-item-badge scheduled">Mar 18, 9:00 AM</span> 495 + </div> 496 + </div> 497 + 498 + <div class="draft-item"> 499 + <div class="draft-item-text">Quick review of the new AT Protocol SDK features that shipped this week</div> 500 + <div class="draft-item-meta"> 501 + <span>3 days ago</span> 502 + <span class="draft-item-badge">Draft</span> 503 + </div> 504 + </div> 505 + 506 + </div> 507 + 508 + <script> 509 + if (localStorage.getItem('theme')) { 510 + const t = localStorage.getItem('theme'); 511 + if (t !== 'light') document.documentElement.setAttribute('data-theme', t); 512 + } 513 + </script> 514 + </body> 515 + </html>
+638
docs/designs/messages.html
··· 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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.2.2/dist/fonts/geist-sans/style.css"> 11 + <link rel="stylesheet" href="styles.css"> 12 + <style> 13 + .messages-container { 14 + padding-bottom: 88px; 15 + } 16 + 17 + /* Tabs */ 18 + .msg-tabs { 19 + display: flex; 20 + border-bottom: 1px solid var(--border); 21 + background-color: var(--bg); 22 + } 23 + 24 + .msg-tab { 25 + flex: 1; 26 + padding: 14px; 27 + text-align: center; 28 + font-weight: 600; 29 + font-size: 15px; 30 + color: var(--text-secondary); 31 + cursor: pointer; 32 + border-bottom: 2px solid transparent; 33 + transition: all 0.2s ease; 34 + background: none; 35 + border-top: none; 36 + border-left: none; 37 + border-right: none; 38 + } 39 + 40 + .msg-tab:hover { 41 + background-color: var(--surface); 42 + color: var(--text-primary); 43 + } 44 + 45 + .msg-tab.active { 46 + color: var(--text-primary); 47 + border-bottom-color: var(--accent-primary); 48 + } 49 + 50 + .msg-tab-badge { 51 + display: inline-flex; 52 + align-items: center; 53 + justify-content: center; 54 + min-width: 18px; 55 + height: 18px; 56 + padding: 0 5px; 57 + border-radius: 9px; 58 + background-color: var(--accent-error); 59 + color: white; 60 + font-size: 10px; 61 + font-weight: 700; 62 + margin-left: 6px; 63 + } 64 + 65 + /* Conversation Item */ 66 + .convo-item { 67 + display: flex; 68 + align-items: center; 69 + gap: 12px; 70 + padding: 14px 16px; 71 + border-bottom: 1px solid var(--border); 72 + cursor: pointer; 73 + transition: background-color 0.2s ease; 74 + position: relative; 75 + } 76 + 77 + .convo-item:hover { 78 + background-color: var(--surface); 79 + } 80 + 81 + .convo-item.unread { 82 + background-color: var(--surface); 83 + } 84 + 85 + .convo-avatar { 86 + position: relative; 87 + flex-shrink: 0; 88 + } 89 + 90 + .convo-avatar .avatar { 91 + width: 48px; 92 + height: 48px; 93 + } 94 + 95 + .convo-unread-dot { 96 + position: absolute; 97 + top: 0; 98 + right: 0; 99 + width: 12px; 100 + height: 12px; 101 + border-radius: 50%; 102 + background-color: var(--accent-primary); 103 + border: 2px solid var(--bg); 104 + } 105 + 106 + .convo-info { 107 + flex: 1; 108 + min-width: 0; 109 + } 110 + 111 + .convo-header { 112 + display: flex; 113 + align-items: baseline; 114 + justify-content: space-between; 115 + gap: 8px; 116 + margin-bottom: 2px; 117 + } 118 + 119 + .convo-name { 120 + font-weight: 600; 121 + font-size: 15px; 122 + color: var(--text-primary); 123 + white-space: nowrap; 124 + overflow: hidden; 125 + text-overflow: ellipsis; 126 + } 127 + 128 + .convo-time { 129 + font-size: 12px; 130 + color: var(--text-muted); 131 + flex-shrink: 0; 132 + } 133 + 134 + .convo-last-message { 135 + font-size: 14px; 136 + color: var(--text-secondary); 137 + white-space: nowrap; 138 + overflow: hidden; 139 + text-overflow: ellipsis; 140 + } 141 + 142 + .convo-last-message.unread { 143 + color: var(--text-primary); 144 + font-weight: 500; 145 + } 146 + 147 + .convo-muted-icon { 148 + width: 14px; 149 + height: 14px; 150 + color: var(--text-muted); 151 + flex-shrink: 0; 152 + } 153 + 154 + /* New Message FAB */ 155 + .fab-new-message { 156 + position: fixed; 157 + bottom: 100px; 158 + right: calc(50% - 207px + 16px); 159 + width: 56px; 160 + height: 56px; 161 + border-radius: 50%; 162 + background-color: var(--accent-primary); 163 + color: white; 164 + border: none; 165 + cursor: pointer; 166 + display: flex; 167 + align-items: center; 168 + justify-content: center; 169 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 170 + transition: all 0.2s ease; 171 + z-index: 50; 172 + } 173 + 174 + .fab-new-message:hover { 175 + transform: scale(1.05); 176 + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); 177 + } 178 + 179 + .fab-new-message svg { 180 + width: 24px; 181 + height: 24px; 182 + } 183 + 184 + /* Message Thread View */ 185 + .thread-header { 186 + display: flex; 187 + align-items: center; 188 + gap: 12px; 189 + padding: 12px 16px; 190 + border-bottom: 1px solid var(--border); 191 + background-color: var(--bg); 192 + position: sticky; 193 + top: 0; 194 + z-index: 50; 195 + } 196 + 197 + .thread-back { 198 + background: none; 199 + border: none; 200 + color: var(--text-secondary); 201 + cursor: pointer; 202 + display: flex; 203 + align-items: center; 204 + } 205 + 206 + .thread-back svg { 207 + width: 24px; 208 + height: 24px; 209 + } 210 + 211 + .thread-info { 212 + flex: 1; 213 + } 214 + 215 + .thread-name { 216 + font-weight: 600; 217 + font-size: 16px; 218 + color: var(--text-primary); 219 + } 220 + 221 + .thread-handle { 222 + font-size: 13px; 223 + color: var(--text-secondary); 224 + } 225 + 226 + .thread-overflow { 227 + background: none; 228 + border: none; 229 + color: var(--text-secondary); 230 + cursor: pointer; 231 + padding: 8px; 232 + } 233 + 234 + .thread-overflow svg { 235 + width: 20px; 236 + height: 20px; 237 + } 238 + 239 + /* Chat Bubbles */ 240 + .chat-area { 241 + padding: 16px; 242 + display: flex; 243 + flex-direction: column; 244 + gap: 8px; 245 + padding-bottom: 80px; 246 + } 247 + 248 + .chat-bubble-row { 249 + display: flex; 250 + gap: 8px; 251 + max-width: 80%; 252 + } 253 + 254 + .chat-bubble-row.sent { 255 + align-self: flex-end; 256 + flex-direction: row-reverse; 257 + } 258 + 259 + .chat-bubble-row.received { 260 + align-self: flex-start; 261 + } 262 + 263 + .chat-bubble { 264 + padding: 10px 14px; 265 + border-radius: 18px; 266 + font-size: 15px; 267 + line-height: 1.4; 268 + position: relative; 269 + } 270 + 271 + .chat-bubble.sent { 272 + background-color: var(--accent-primary); 273 + color: white; 274 + border-bottom-right-radius: 4px; 275 + } 276 + 277 + .chat-bubble.received { 278 + background-color: var(--surface); 279 + color: var(--text-primary); 280 + border: 1px solid var(--border); 281 + border-bottom-left-radius: 4px; 282 + } 283 + 284 + .chat-time { 285 + font-size: 11px; 286 + color: var(--text-muted); 287 + margin-top: 4px; 288 + padding: 0 4px; 289 + } 290 + 291 + .chat-time.sent { 292 + text-align: right; 293 + } 294 + 295 + .chat-date-divider { 296 + text-align: center; 297 + padding: 12px 0; 298 + font-size: 12px; 299 + color: var(--text-muted); 300 + font-weight: 500; 301 + } 302 + 303 + /* Message Input */ 304 + .msg-input-bar { 305 + position: fixed; 306 + bottom: 0; 307 + left: 50%; 308 + transform: translateX(-50%); 309 + width: 100%; 310 + max-width: 414px; 311 + display: flex; 312 + align-items: center; 313 + gap: 8px; 314 + padding: 12px 16px; 315 + background-color: var(--bg); 316 + border-top: 1px solid var(--border); 317 + z-index: 100; 318 + } 319 + 320 + .msg-input { 321 + flex: 1; 322 + padding: 10px 16px; 323 + border: 1px solid var(--border); 324 + border-radius: 9999px; 325 + background-color: var(--surface); 326 + color: var(--text-primary); 327 + font-size: 15px; 328 + font-family: var(--font-body); 329 + outline: none; 330 + transition: border-color 0.2s ease; 331 + } 332 + 333 + .msg-input:focus { 334 + border-color: var(--accent-primary); 335 + } 336 + 337 + .msg-input::placeholder { 338 + color: var(--text-muted); 339 + } 340 + 341 + .msg-send-btn { 342 + width: 40px; 343 + height: 40px; 344 + border-radius: 50%; 345 + border: none; 346 + background-color: var(--accent-primary); 347 + color: white; 348 + cursor: pointer; 349 + display: flex; 350 + align-items: center; 351 + justify-content: center; 352 + transition: background-color 0.2s ease; 353 + flex-shrink: 0; 354 + } 355 + 356 + .msg-send-btn:hover { 357 + background-color: var(--accent-primary-hover); 358 + } 359 + 360 + .msg-send-btn svg { 361 + width: 18px; 362 + height: 18px; 363 + } 364 + 365 + /* Nav badge */ 366 + .nav-item { 367 + position: relative; 368 + } 369 + 370 + .nav-item-badge { 371 + position: absolute; 372 + top: 2px; 373 + right: 8px; 374 + min-width: 18px; 375 + height: 18px; 376 + padding: 0 5px; 377 + border-radius: 9px; 378 + background-color: var(--accent-error); 379 + color: white; 380 + font-size: 10px; 381 + font-weight: 700; 382 + display: flex; 383 + align-items: center; 384 + justify-content: center; 385 + } 386 + 387 + /* View toggle */ 388 + .view-toggle { display: none; } 389 + .view-toggle.active { display: block; } 390 + </style> 391 + </head> 392 + <body> 393 + <div class="mobile-container"> 394 + 395 + <!-- ==================== --> 396 + <!-- CONVERSATION LIST VIEW --> 397 + <!-- ==================== --> 398 + <div class="view-toggle active" id="list-view"> 399 + 400 + <!-- Header --> 401 + <header class="header"> 402 + <h1 class="header-title">Messages</h1> 403 + <button class="header-action" onclick="toggleView()">Open Thread</button> 404 + </header> 405 + 406 + <!-- Tabs --> 407 + <div class="msg-tabs"> 408 + <button class="msg-tab active">Primary</button> 409 + <button class="msg-tab">Requests <span class="msg-tab-badge">2</span></button> 410 + </div> 411 + 412 + <div class="messages-container"> 413 + 414 + <!-- Unread conversation --> 415 + <div class="convo-item unread"> 416 + <div class="convo-avatar"> 417 + <div class="avatar">AS</div> 418 + <div class="convo-unread-dot"></div> 419 + </div> 420 + <div class="convo-info"> 421 + <div class="convo-header"> 422 + <span class="convo-name">Alice Smith</span> 423 + <span class="convo-time">12m</span> 424 + </div> 425 + <div class="convo-last-message unread">Hey! Did you see the new federation update? It's really exciting.</div> 426 + </div> 427 + </div> 428 + 429 + <!-- Unread conversation --> 430 + <div class="convo-item unread"> 431 + <div class="convo-avatar"> 432 + <div class="avatar">BJ</div> 433 + <div class="convo-unread-dot"></div> 434 + </div> 435 + <div class="convo-info"> 436 + <div class="convo-header"> 437 + <span class="convo-name">Bob Johnson</span> 438 + <span class="convo-time">2h</span> 439 + </div> 440 + <div class="convo-last-message unread">Would love to collaborate on the AT Protocol project</div> 441 + </div> 442 + </div> 443 + 444 + <!-- Read conversation --> 445 + <div class="convo-item"> 446 + <div class="convo-avatar"> 447 + <div class="avatar">CW</div> 448 + </div> 449 + <div class="convo-info"> 450 + <div class="convo-header"> 451 + <span class="convo-name">Carol White</span> 452 + <span class="convo-time">1d</span> 453 + </div> 454 + <div class="convo-last-message">Thanks for sharing that article! I'll check it out this weekend.</div> 455 + </div> 456 + </div> 457 + 458 + <!-- Muted conversation --> 459 + <div class="convo-item"> 460 + <div class="convo-avatar"> 461 + <div class="avatar">DM</div> 462 + </div> 463 + <div class="convo-info"> 464 + <div class="convo-header"> 465 + <span class="convo-name">David Miller</span> 466 + <span class="convo-time">3d</span> 467 + </div> 468 + <div class="convo-last-message">Sounds good, let's catch up next week then!</div> 469 + </div> 470 + <svg class="convo-muted-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 471 + <path d="M11 5L6 9H2v6h4l5 4V5z"/> 472 + <line x1="23" y1="9" x2="17" y2="15"/> 473 + <line x1="17" y1="9" x2="23" y2="15"/> 474 + </svg> 475 + </div> 476 + 477 + <!-- Old conversation --> 478 + <div class="convo-item"> 479 + <div class="convo-avatar"> 480 + <div class="avatar">EL</div> 481 + </div> 482 + <div class="convo-info"> 483 + <div class="convo-header"> 484 + <span class="convo-name">Eva Lee</span> 485 + <span class="convo-time">1w</span> 486 + </div> 487 + <div class="convo-last-message">Great chatting with you at the meetup!</div> 488 + </div> 489 + </div> 490 + 491 + </div> 492 + 493 + <!-- New Message FAB --> 494 + <button class="fab-new-message" title="New message"> 495 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 496 + <line x1="22" y1="2" x2="11" y2="13"/> 497 + <polygon points="22 2 15 22 11 13 2 9 22 2"/> 498 + </svg> 499 + </button> 500 + 501 + <!-- Bottom Navigation --> 502 + <nav class="nav-bar"> 503 + <a href="home.html" class="nav-item"> 504 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 505 + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/> 506 + <polyline points="9 22 9 12 15 12 15 22"/> 507 + </svg> 508 + <span>Home</span> 509 + </a> 510 + 511 + <a href="search.html" class="nav-item"> 512 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 513 + <circle cx="11" cy="11" r="8"/> 514 + <line x1="21" y1="21" x2="16.65" y2="16.65"/> 515 + </svg> 516 + <span>Search</span> 517 + </a> 518 + 519 + <a href="notifications.html" class="nav-item"> 520 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 521 + <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/> 522 + <path d="M13.73 21a2 2 0 0 1-3.46 0"/> 523 + </svg> 524 + <span>Alerts</span> 525 + </a> 526 + 527 + <a href="messages.html" class="nav-item active"> 528 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 529 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 530 + </svg> 531 + <span class="nav-item-badge">2</span> 532 + <span>Chat</span> 533 + </a> 534 + 535 + <a href="profile.html" class="nav-item"> 536 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 537 + <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 538 + <circle cx="12" cy="7" r="4"/> 539 + </svg> 540 + <span>Profile</span> 541 + </a> 542 + </nav> 543 + </div> 544 + 545 + <!-- ==================== --> 546 + <!-- MESSAGE THREAD VIEW --> 547 + <!-- ==================== --> 548 + <div class="view-toggle" id="thread-view"> 549 + 550 + <!-- Thread Header --> 551 + <div class="thread-header"> 552 + <button class="thread-back" onclick="toggleView()"> 553 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 554 + <polyline points="15 18 9 12 15 6"/> 555 + </svg> 556 + </button> 557 + <div class="thread-info"> 558 + <div class="thread-name">Alice Smith</div> 559 + <div class="thread-handle">@alice.bsky.social</div> 560 + </div> 561 + <button class="thread-overflow"> 562 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 563 + <circle cx="12" cy="12" r="1"/> 564 + <circle cx="19" cy="12" r="1"/> 565 + <circle cx="5" cy="12" r="1"/> 566 + </svg> 567 + </button> 568 + </div> 569 + 570 + <!-- Chat Area --> 571 + <div class="chat-area"> 572 + 573 + <div class="chat-date-divider">Today</div> 574 + 575 + <div class="chat-bubble-row received"> 576 + <div class="chat-bubble received">Hey! Did you see the new federation update? It's really exciting.</div> 577 + </div> 578 + <div class="chat-time">10:23 AM</div> 579 + 580 + <div class="chat-bubble-row sent"> 581 + <div class="chat-bubble sent">Yes! I was just reading through the announcement. The self-hosting guide looks much improved.</div> 582 + </div> 583 + <div class="chat-time sent">10:25 AM</div> 584 + 585 + <div class="chat-bubble-row received"> 586 + <div class="chat-bubble received">Right? I'm thinking of setting up my own PDS this weekend. Want to try it together?</div> 587 + </div> 588 + <div class="chat-time">10:27 AM</div> 589 + 590 + <div class="chat-bubble-row sent"> 591 + <div class="chat-bubble sent">That sounds great! I've been meaning to do that for a while. Let me know when you're free.</div> 592 + </div> 593 + <div class="chat-time sent">10:30 AM</div> 594 + 595 + <div class="chat-bubble-row received"> 596 + <div class="chat-bubble received">Saturday afternoon works for me. We could do a video call and set them up at the same time.</div> 597 + </div> 598 + <div class="chat-time">10:32 AM</div> 599 + 600 + </div> 601 + 602 + <!-- Message Input --> 603 + <div class="msg-input-bar"> 604 + <input class="msg-input" type="text" placeholder="Type a message..."> 605 + <button class="msg-send-btn"> 606 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 607 + <line x1="22" y1="2" x2="11" y2="13"/> 608 + <polygon points="22 2 15 22 11 13 2 9 22 2"/> 609 + </svg> 610 + </button> 611 + </div> 612 + 613 + </div> 614 + 615 + </div> 616 + 617 + <script> 618 + if (localStorage.getItem('theme')) { 619 + const t = localStorage.getItem('theme'); 620 + if (t !== 'light') document.documentElement.setAttribute('data-theme', t); 621 + } 622 + 623 + // Toggle between list and thread views for demo 624 + function toggleView() { 625 + document.getElementById('list-view').classList.toggle('active'); 626 + document.getElementById('thread-view').classList.toggle('active'); 627 + } 628 + 629 + // Tab switching demo 630 + document.querySelectorAll('.msg-tab').forEach(tab => { 631 + tab.addEventListener('click', () => { 632 + document.querySelectorAll('.msg-tab').forEach(t => t.classList.remove('active')); 633 + tab.classList.add('active'); 634 + }); 635 + }); 636 + </script> 637 + </body> 638 + </html>
+461
docs/designs/notifications.html
··· 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>Notifications - 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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/geist@1.2.2/dist/fonts/geist-sans/style.css" /> 11 + <link rel="stylesheet" href="styles.css" /> 12 + <style> 13 + .notif-container { 14 + padding-bottom: 88px; 15 + } 16 + 17 + /* Day Group */ 18 + .notif-day-header { 19 + padding: 12px 16px 8px; 20 + font-size: 13px; 21 + font-weight: 600; 22 + color: var(--text-muted); 23 + text-transform: uppercase; 24 + letter-spacing: 0.5px; 25 + background-color: var(--bg); 26 + border-bottom: 1px solid var(--border); 27 + } 28 + 29 + /* Notification Item */ 30 + .notif-item { 31 + display: flex; 32 + gap: 12px; 33 + padding: 14px 16px; 34 + border-bottom: 1px solid var(--border); 35 + cursor: pointer; 36 + transition: background-color 0.2s ease; 37 + } 38 + 39 + .notif-item:hover { 40 + background-color: var(--surface); 41 + } 42 + 43 + .notif-item.unread { 44 + background-color: var(--surface); 45 + } 46 + 47 + .notif-item.unread::before { 48 + content: ""; 49 + position: absolute; 50 + left: 0; 51 + top: 0; 52 + bottom: 0; 53 + width: 3px; 54 + background-color: var(--accent-primary); 55 + } 56 + 57 + .notif-item { 58 + position: relative; 59 + } 60 + 61 + /* Reason Icon */ 62 + .notif-icon { 63 + width: 32px; 64 + height: 32px; 65 + border-radius: 50%; 66 + display: flex; 67 + align-items: center; 68 + justify-content: center; 69 + flex-shrink: 0; 70 + } 71 + 72 + .notif-icon svg { 73 + width: 16px; 74 + height: 16px; 75 + } 76 + 77 + .notif-icon-like { 78 + background-color: rgba(239, 68, 68, 0.1); 79 + color: var(--accent-error); 80 + } 81 + 82 + .notif-icon-repost { 83 + background-color: rgba(34, 197, 94, 0.1); 84 + color: var(--accent-success); 85 + } 86 + 87 + .notif-icon-follow { 88 + background-color: rgba(0, 102, 255, 0.1); 89 + color: var(--accent-primary); 90 + } 91 + 92 + .notif-icon-reply { 93 + background-color: rgba(14, 165, 233, 0.1); 94 + color: var(--accent-secondary); 95 + } 96 + 97 + .notif-icon-mention { 98 + background-color: rgba(0, 102, 255, 0.1); 99 + color: var(--accent-primary); 100 + } 101 + 102 + .notif-icon-quote { 103 + background-color: rgba(139, 92, 246, 0.1); 104 + color: #8b5cf6; 105 + } 106 + 107 + /* Content */ 108 + .notif-content { 109 + flex: 1; 110 + min-width: 0; 111 + } 112 + 113 + .notif-actor { 114 + display: flex; 115 + align-items: center; 116 + gap: 8px; 117 + margin-bottom: 4px; 118 + } 119 + 120 + .notif-actor-avatar { 121 + width: 28px; 122 + height: 28px; 123 + border-radius: 50%; 124 + background-color: var(--surface-variant); 125 + display: flex; 126 + align-items: center; 127 + justify-content: center; 128 + font-size: 11px; 129 + font-weight: 600; 130 + color: var(--text-secondary); 131 + flex-shrink: 0; 132 + } 133 + 134 + .notif-summary { 135 + font-size: 14px; 136 + color: var(--text-primary); 137 + line-height: 1.4; 138 + } 139 + 140 + .notif-summary strong { 141 + font-weight: 600; 142 + } 143 + 144 + .notif-summary .reason { 145 + color: var(--text-secondary); 146 + } 147 + 148 + .notif-time { 149 + font-size: 12px; 150 + color: var(--text-muted); 151 + margin-top: 2px; 152 + } 153 + 154 + .notif-preview { 155 + margin-top: 8px; 156 + padding: 10px 12px; 157 + background-color: var(--surface); 158 + border-radius: 8px; 159 + border: 1px solid var(--border); 160 + font-size: 13px; 161 + color: var(--text-secondary); 162 + line-height: 1.4; 163 + display: -webkit-box; 164 + line-clamp: 2; 165 + -webkit-line-clamp: 2; 166 + -webkit-box-orient: vertical; 167 + overflow: hidden; 168 + } 169 + 170 + /* Unread badge for nav */ 171 + .nav-item-badge { 172 + position: absolute; 173 + top: 2px; 174 + right: 8px; 175 + min-width: 18px; 176 + height: 18px; 177 + padding: 0 5px; 178 + border-radius: 9px; 179 + background-color: var(--accent-error); 180 + color: white; 181 + font-size: 10px; 182 + font-weight: 700; 183 + display: flex; 184 + align-items: center; 185 + justify-content: center; 186 + } 187 + 188 + .nav-item { 189 + position: relative; 190 + } 191 + </style> 192 + </head> 193 + <body> 194 + <div class="mobile-container"> 195 + <!-- Header --> 196 + <header class="header"> 197 + <h1 class="header-title">Notifications</h1> 198 + <button class="header-action">Mark All Read</button> 199 + </header> 200 + 201 + <div class="notif-container"> 202 + <!-- Today --> 203 + <div class="notif-day-header">Today</div> 204 + 205 + <!-- Like notification (unread) --> 206 + <div class="notif-item unread"> 207 + <div class="notif-icon notif-icon-like"> 208 + <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"> 209 + <path 210 + d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> 211 + </svg> 212 + </div> 213 + <div class="notif-content"> 214 + <div class="notif-actor"> 215 + <div class="notif-actor-avatar">AS</div> 216 + </div> 217 + <div class="notif-summary"><strong>Alice Smith</strong> <span class="reason">liked your post</span></div> 218 + <div class="notif-time">12 minutes ago</div> 219 + <div class="notif-preview"> 220 + Excited to share my latest project built with the AT Protocol! Check it out and let me know what you 221 + think. 222 + </div> 223 + </div> 224 + </div> 225 + 226 + <!-- Follow notification (unread) --> 227 + <div class="notif-item unread"> 228 + <div class="notif-icon notif-icon-follow"> 229 + <svg 230 + viewBox="0 0 24 24" 231 + fill="none" 232 + stroke="currentColor" 233 + stroke-width="2" 234 + stroke-linecap="round" 235 + stroke-linejoin="round"> 236 + <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /> 237 + <circle cx="8.5" cy="7" r="4" /> 238 + <line x1="20" y1="8" x2="20" y2="14" /> 239 + <line x1="23" y1="11" x2="17" y2="11" /> 240 + </svg> 241 + </div> 242 + <div class="notif-content"> 243 + <div class="notif-actor"> 244 + <div class="notif-actor-avatar">BJ</div> 245 + </div> 246 + <div class="notif-summary"><strong>Bob Johnson</strong> <span class="reason">followed you</span></div> 247 + <div class="notif-time">45 minutes ago</div> 248 + </div> 249 + </div> 250 + 251 + <!-- Reply notification (unread) --> 252 + <div class="notif-item unread"> 253 + <div class="notif-icon notif-icon-reply"> 254 + <svg 255 + viewBox="0 0 24 24" 256 + fill="none" 257 + stroke="currentColor" 258 + stroke-width="2" 259 + stroke-linecap="round" 260 + stroke-linejoin="round"> 261 + <path 262 + d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" /> 263 + </svg> 264 + </div> 265 + <div class="notif-content"> 266 + <div class="notif-actor"> 267 + <div class="notif-actor-avatar">CW</div> 268 + </div> 269 + <div class="notif-summary"> 270 + <strong>Carol White</strong> <span class="reason">replied to your post</span> 271 + </div> 272 + <div class="notif-time">1 hour ago</div> 273 + <div class="notif-preview"> 274 + This looks amazing! I've been working on something similar with the federation layer. Would love to 275 + compare notes. 276 + </div> 277 + </div> 278 + </div> 279 + 280 + <!-- Repost notification --> 281 + <div class="notif-item"> 282 + <div class="notif-icon notif-icon-repost"> 283 + <svg 284 + viewBox="0 0 24 24" 285 + fill="none" 286 + stroke="currentColor" 287 + stroke-width="2" 288 + stroke-linecap="round" 289 + stroke-linejoin="round"> 290 + <polyline points="17 1 21 5 17 9" /> 291 + <path d="M3 11V9a4 4 0 0 1 4-4h14" /> 292 + <polyline points="7 23 3 19 7 15" /> 293 + <path d="M21 13v2a4 4 0 0 1-4 4H3" /> 294 + </svg> 295 + </div> 296 + <div class="notif-content"> 297 + <div class="notif-actor"> 298 + <div class="notif-actor-avatar">DM</div> 299 + </div> 300 + <div class="notif-summary"> 301 + <strong>David Miller</strong> <span class="reason">reposted your post</span> 302 + </div> 303 + <div class="notif-time">3 hours ago</div> 304 + <div class="notif-preview">Great read on the future of decentralised social networks</div> 305 + </div> 306 + </div> 307 + 308 + <!-- Yesterday --> 309 + <div class="notif-day-header">Yesterday</div> 310 + 311 + <!-- Quote notification --> 312 + <div class="notif-item"> 313 + <div class="notif-icon notif-icon-quote"> 314 + <svg 315 + viewBox="0 0 24 24" 316 + fill="none" 317 + stroke="currentColor" 318 + stroke-width="2" 319 + stroke-linecap="round" 320 + stroke-linejoin="round"> 321 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> 322 + </svg> 323 + </div> 324 + <div class="notif-content"> 325 + <div class="notif-actor"> 326 + <div class="notif-actor-avatar">EL</div> 327 + </div> 328 + <div class="notif-summary"><strong>Eva Lee</strong> <span class="reason">quoted your post</span></div> 329 + <div class="notif-time">18 hours ago</div> 330 + <div class="notif-preview"> 331 + Completely agree with this take. The open protocol approach is really paying off for the whole ecosystem. 332 + </div> 333 + </div> 334 + </div> 335 + 336 + <!-- Mention notification --> 337 + <div class="notif-item"> 338 + <div class="notif-icon notif-icon-mention"> 339 + <svg 340 + viewBox="0 0 24 24" 341 + fill="none" 342 + stroke="currentColor" 343 + stroke-width="2" 344 + stroke-linecap="round" 345 + stroke-linejoin="round"> 346 + <circle cx="12" cy="12" r="4" /> 347 + <path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94" /> 348 + </svg> 349 + </div> 350 + <div class="notif-content"> 351 + <div class="notif-actor"> 352 + <div class="notif-actor-avatar">FG</div> 353 + </div> 354 + <div class="notif-summary"><strong>Frank Garcia</strong> <span class="reason">mentioned you</span></div> 355 + <div class="notif-time">Yesterday</div> 356 + <div class="notif-preview"> 357 + Hey @johndoe.bsky.social have you tried the new SDK features? They shipped some great improvements. 358 + </div> 359 + </div> 360 + </div> 361 + 362 + <!-- Like notification --> 363 + <div class="notif-item"> 364 + <div class="notif-icon notif-icon-like"> 365 + <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"> 366 + <path 367 + d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" /> 368 + </svg> 369 + </div> 370 + <div class="notif-content"> 371 + <div class="notif-actor"> 372 + <div class="notif-actor-avatar">GH</div> 373 + </div> 374 + <div class="notif-summary"><strong>Grace Huang</strong> <span class="reason">liked your post</span></div> 375 + <div class="notif-time">Yesterday</div> 376 + </div> 377 + </div> 378 + </div> 379 + 380 + <!-- Bottom Navigation (5-tab layout) --> 381 + <nav class="nav-bar"> 382 + <a href="home.html" class="nav-item"> 383 + <svg 384 + viewBox="0 0 24 24" 385 + fill="none" 386 + stroke="currentColor" 387 + stroke-width="2" 388 + stroke-linecap="round" 389 + stroke-linejoin="round"> 390 + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> 391 + <polyline points="9 22 9 12 15 12 15 22" /> 392 + </svg> 393 + <span>Home</span> 394 + </a> 395 + 396 + <a href="search.html" class="nav-item"> 397 + <svg 398 + viewBox="0 0 24 24" 399 + fill="none" 400 + stroke="currentColor" 401 + stroke-width="2" 402 + stroke-linecap="round" 403 + stroke-linejoin="round"> 404 + <circle cx="11" cy="11" r="8" /> 405 + <line x1="21" y1="21" x2="16.65" y2="16.65" /> 406 + </svg> 407 + <span>Search</span> 408 + </a> 409 + 410 + <a href="notifications.html" class="nav-item active"> 411 + <svg 412 + viewBox="0 0 24 24" 413 + fill="none" 414 + stroke="currentColor" 415 + stroke-width="2" 416 + stroke-linecap="round" 417 + stroke-linejoin="round"> 418 + <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" /> 419 + <path d="M13.73 21a2 2 0 0 1-3.46 0" /> 420 + </svg> 421 + <span class="nav-item-badge">3</span> 422 + <span>Alerts</span> 423 + </a> 424 + 425 + <a href="messages.html" class="nav-item"> 426 + <svg 427 + viewBox="0 0 24 24" 428 + fill="none" 429 + stroke="currentColor" 430 + stroke-width="2" 431 + stroke-linecap="round" 432 + stroke-linejoin="round"> 433 + <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> 434 + </svg> 435 + <span>Chat</span> 436 + </a> 437 + 438 + <a href="profile.html" class="nav-item"> 439 + <svg 440 + viewBox="0 0 24 24" 441 + fill="none" 442 + stroke="currentColor" 443 + stroke-width="2" 444 + stroke-linecap="round" 445 + stroke-linejoin="round"> 446 + <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> 447 + <circle cx="12" cy="7" r="4" /> 448 + </svg> 449 + <span>Profile</span> 450 + </a> 451 + </nav> 452 + </div> 453 + 454 + <script> 455 + if (localStorage.getItem("theme")) { 456 + const t = localStorage.getItem("theme"); 457 + if (t !== "light") document.documentElement.setAttribute("data-theme", t); 458 + } 459 + </script> 460 + </body> 461 + </html>
+279
docs/specs/phase-3.md
··· 1 + # Phase 3 2 + 3 + ## Post Composition 4 + 5 + Posts are created via `com.atproto.repo.createRecord` with collection 6 + `app.bsky.feed.post`. The compose screen is a full-screen modal opened from a 7 + floating action button on the home screen. 8 + 9 + ### Post Record Structure 10 + 11 + | Field | Type | Description | 12 + | ----------- | -------- | --------------------------------------------- | 13 + | `text` | string | Post body, max 300 graphemes | 14 + | `facets` | array | Rich text annotations (mentions, links, tags) | 15 + | `embed` | union | Attached media, link card, or quote post | 16 + | `reply` | object | `parent` + `root` refs for threaded replies | 17 + | `langs` | array | BCP-47 language tags | 18 + | `createdAt` | datetime | ISO 8601 timestamp | 19 + 20 + ### Media Uploads 21 + 22 + Upload images via `com.atproto.repo.uploadBlob`. Returns a `blob` ref used in 23 + the embed object. No GIF support — images only. 24 + 25 + | Constraint | Value | 26 + | -------------- | ---------------------------------- | 27 + | Max images | 4 per post | 28 + | Max file size | 1 MB per image | 29 + | Accepted types | JPEG, PNG, WebP | 30 + | Alt text | Required UI field, optional in API | 31 + 32 + Embed type for images: `app.bsky.embed.images`. Each image entry has `image` 33 + (blob ref), `alt` (string), and optional `aspectRatio` (`width` / `height`). 34 + 35 + ### Facet Detection 36 + 37 + Use **bluesky_text** to detect mentions, links, and hashtags in the post text 38 + and produce the `facets` array automatically before submission. The compose 39 + screen should render a live preview of detected facets with colour-coded 40 + highlights as the user types. 41 + 42 + ### Grapheme Counter 43 + 44 + Display a live character counter showing remaining graphemes (300 max). Use 45 + Dart's `Characters` class for accurate grapheme cluster counting. Disable the 46 + submit button when the count exceeds 300 or the text is empty. 47 + 48 + ### Drafts 49 + 50 + Persist unsent posts locally in a Drift `drafts` table. On network failure or 51 + explicit save, always store the draft. Drafts are account-scoped. 52 + 53 + | Column | Type | Notes | 54 + | ------------- | -------- | ---------------------------------------- | 55 + | `id` | integer | PK autoincrement | 56 + | `account_did` | text | FK to `accounts` | 57 + | `text` | text | Post body | 58 + | `reply_uri` | text | Nullable; parent post URI if reply | 59 + | `embed_json` | text | Nullable; serialised embed data | 60 + | `media_paths` | text | Nullable; JSON array of local file paths | 61 + | `created_at` | datetime | When the draft was created | 62 + | `updated_at` | datetime | Last modification | 63 + 64 + Display a "Drafts" entry in the compose screen accessible via a toolbar icon. 65 + Tapping a draft loads it back into the composer for editing / sending. 66 + 67 + ### Scheduled Posts 68 + 69 + Schedule posts for future publication using a local scheduler. Store the 70 + scheduled time alongside the draft. Use a `WorkManager` (Android) / 71 + `BGTaskScheduler` (iOS) background task to submit the post at the scheduled 72 + time. If the device is offline at the scheduled time, queue the post and retry 73 + when connectivity resumes. 74 + 75 + Add a `scheduled_at` (nullable datetime) column to the `drafts` table. When 76 + non-null, the draft is treated as scheduled rather than a regular draft. 77 + 78 + Build a `ComposeBloc` with events: `TextChanged`, `MediaAttached`, 79 + `MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`, 80 + `PostScheduled`, `PostSubmitted`. 81 + 82 + ## Notifications 83 + 84 + Render BlueSky notifications using `app.bsky.notification.listNotifications`. 85 + No push notifications in this phase — polling only. 86 + 87 + ### API 88 + 89 + | Endpoint | Purpose | 90 + | ----------------------------------------- | --------------------------- | 91 + | `app.bsky.notification.listNotifications` | Paginated notification list | 92 + | `app.bsky.notification.updateSeen` | Mark notifications as read | 93 + | `app.bsky.notification.getUnreadCount` | Badge count for nav bar | 94 + 95 + `listNotifications` returns `notification` objects with `reason`, `author`, 96 + `record`, `isRead`, `indexedAt`. Paginate with `cursor`; `limit` 1–100. 97 + 98 + ### Notification Reasons 99 + 100 + | Reason | Display | 101 + | --------- | ---------------------------------------------- | 102 + | `like` | "[Author] liked your post" + post preview | 103 + | `repost` | "[Author] reposted your post" + post preview | 104 + | `follow` | "[Author] followed you" | 105 + | `mention` | "[Author] mentioned you" + post preview | 106 + | `reply` | "[Author] replied to your post" + post preview | 107 + | `quote` | "[Author] quoted your post" + post preview | 108 + 109 + ### Rendering 110 + 111 + Group notifications by day. Each notification row shows the author avatar, the 112 + reason icon, a summary line, and an optional post preview snippet. Tapping a 113 + notification navigates to the relevant post or profile. 114 + 115 + Display an unread count badge on the Notifications nav bar item. Poll 116 + `getUnreadCount` on a 30-second interval when the app is foregrounded. Call 117 + `updateSeen` when the notifications screen is opened. 118 + 119 + Build a `NotificationBloc` with events: `NotificationsRequested`, 120 + `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead`. 121 + 122 + ## Direct Messages 123 + 124 + DMs use the `chat.bsky.*` lexicon namespace. The DM feature has two views: a 125 + conversation list and a message thread. 126 + 127 + ### API 128 + 129 + | Endpoint | Purpose | 130 + | -------------------------------------- | ----------------------------- | 131 + | `chat.bsky.convo.listConvos` | Paginated conversation list | 132 + | `chat.bsky.convo.getConvo` | Single conversation metadata | 133 + | `chat.bsky.convo.getMessages` | Paginated messages in a convo | 134 + | `chat.bsky.convo.sendMessage` | Send a message | 135 + | `chat.bsky.convo.deleteMessageForSelf` | Delete a message locally | 136 + | `chat.bsky.convo.muteConvo` | Mute a conversation | 137 + | `chat.bsky.convo.unmuteConvo` | Unmute a conversation | 138 + | `chat.bsky.convo.updateRead` | Mark conversation as read | 139 + | `chat.bsky.convo.getLog` | Polling for new events | 140 + 141 + ### Conversation List 142 + 143 + `listConvos` returns conversations sorted by last message time. Each convo 144 + includes `id`, `members` (array of `profileViewBasic`), `lastMessage`, 145 + `unreadCount`, `muted`. 146 + 147 + Filter conversations into two tabs: **Primary** (accepted) and **Requests** 148 + (conversations the user has not yet responded to). A conversation is a 149 + "request" if the user has never sent a message in it. 150 + 151 + ### Message Thread 152 + 153 + `getMessages` returns paginated `messageView` objects. Each message has `id`, 154 + `text`, `sender` (DID), `sentAt`. Messages are displayed in a standard chat 155 + bubble layout — the current user's messages right-aligned, others left-aligned. 156 + 157 + Support long-press to copy individual messages. Provide a "Copy All" option in 158 + the conversation overflow menu to copy the full thread. 159 + 160 + ### Sending Messages 161 + 162 + `sendMessage` takes `convoId` and `message` (object with `text`). To start a 163 + new conversation, the app calls `chat.bsky.convo.getConvoForMembers` with the 164 + target DID(s) — this returns an existing convo or creates a new one. 165 + 166 + | Endpoint | Purpose | 167 + | ------------------------------------ | --------------------- | 168 + | `chat.bsky.convo.getConvoForMembers` | Get or create a convo | 169 + 170 + Build a `ConvoListBloc` with events: `ConvosRequested`, `ConvosRefreshed`, 171 + `ConvoMuted`, `ConvoUnmuted`. 172 + 173 + Build a `MessageBloc` with events: `MessagesRequested`, `MessagesPageLoaded`, 174 + `MessageSent`, `MessageDeleted`, `ConvoMarkedRead`. 175 + 176 + ## Account Switching 177 + 178 + Support multiple authenticated accounts with full data isolation. The 179 + `accounts` table (from Phase 1) already supports multiple rows keyed by DID. 180 + 181 + ### Active Account 182 + 183 + Store the active account DID in the Drift `settings` table under key 184 + `active_account_did`. On launch, read this value and restore the session for 185 + that account. 186 + 187 + ### Data Isolation 188 + 189 + All user-scoped tables must include an `account_did` FK column. Queries always 190 + filter by the active account's DID. Tables requiring this constraint: 191 + 192 + - `drafts` 193 + - `saved_posts` 194 + - `search_history` (Phase 2) 195 + - `cached_posts` (add `account_did` if not present) 196 + 197 + ### Switching Flow 198 + 199 + 1. User opens account switcher (bottom sheet or settings). 200 + 2. Selects a different account. 201 + 3. App updates `active_account_did` in settings. 202 + 4. All Blocs receive a `AccountSwitched` event and reload their state for the 203 + new account. 204 + 5. If the selected account's tokens are expired, attempt silent refresh. If 205 + refresh fails, navigate to login. 206 + 207 + ### Adding Accounts 208 + 209 + "Add Account" triggers the same OAuth flow from Phase 1. On success, a new row 210 + is inserted into `accounts`. The new account becomes the active account. 211 + 212 + Build an `AccountSwitcherCubit` that exposes the list of accounts and the 213 + active DID. 214 + 215 + ## Offline Reading 216 + 217 + The app should render cached data when the network is unavailable. This builds 218 + on the `cached_posts` and `cached_profiles` tables from Phase 1. 219 + 220 + ### Cache Strategy 221 + 222 + Cache the last-fetched page of each feed (timeline, pinned generators) in Drift 223 + as serialised JSON. On launch or feed switch, display cached data immediately, 224 + then fetch fresh data in the background. If the fetch fails, keep showing the 225 + cache with a "You're offline" banner. 226 + 227 + ### Offline Indicators 228 + 229 + - A persistent banner at the top of the screen when connectivity is lost. 230 + - Disable actions that require network (compose, like, repost, follow) and show 231 + a tooltip explaining why. 232 + - Notifications and DM screens show an empty state with "No connection" when 233 + offline and no cached data exists. 234 + 235 + ### Network Detection 236 + 237 + Use the **connectivity_plus** package to monitor network state changes. Expose 238 + connectivity as a stream via a `ConnectivityCubit` that all screens observe. 239 + 240 + ## Saved Posts 241 + 242 + Allow users to bookmark posts locally for later reading. Saved posts are stored 243 + only in Drift — nothing is written to the network. This is intentionally 244 + private and local-only. 245 + 246 + ### Drift Table 247 + 248 + | Column | Type | Notes | 249 + | ------------- | -------- | ---------------------------- | 250 + | `id` | integer | PK autoincrement | 251 + | `account_did` | text | FK to `accounts` | 252 + | `post_uri` | text | AT-URI of the saved post | 253 + | `post_json` | text | Full serialised post payload | 254 + | `saved_at` | datetime | When the user saved the post | 255 + 256 + Unique constraint on (`account_did`, `post_uri`). 257 + 258 + ### UI 259 + 260 + Add a "Save" action (bookmark icon) to the post action bar. Tapping toggles 261 + the saved state. Saved posts are viewable from a "Saved" section in the 262 + profile screen or settings. 263 + 264 + Build a `SavedPostsCubit` that reads/writes the `saved_posts` table and 265 + exposes a stream of saved post URIs for quick lookup (to show filled vs 266 + outlined bookmark icons in the feed). 267 + 268 + ## Jump to Profile 269 + 270 + Add a floating action button on the search screen. Tapping it opens a dialog 271 + with a text field for entering a handle. Use 272 + `app.bsky.actor.searchActorsTypeahead` to provide autocomplete suggestions as 273 + the user types. Selecting a result or pressing enter navigates to that user's 274 + profile screen. 275 + 276 + | Endpoint | Purpose | 277 + | -------------------------------------- | ------------------- | 278 + | `app.bsky.actor.searchActorsTypeahead` | Handle autocomplete | 279 + | `app.bsky.actor.getProfile` | Full profile fetch |
+74
docs/tasks/phase-3.md
··· 1 + # Phase 3 Milestones 2 + 3 + ## M8 — Post Composition 4 + 5 + - [ ] Full-screen compose modal with text input, live grapheme counter (300 max), and submit button 6 + - [ ] `ComposeBloc` — events: `TextChanged`, `MediaAttached`, `MediaRemoved`, `AltTextUpdated`, `DraftSaved`, `DraftLoaded`, `PostScheduled`, `PostSubmitted` 7 + - [ ] Image attachment via `uploadBlob` — up to 4 images, alt text input per image 8 + - [ ] Live facet detection and preview via `bluesky_text` (mentions, links, hashtags) 9 + - [ ] Post creation via `com.atproto.repo.createRecord` with `app.bsky.feed.post` collection 10 + - [ ] Reply support — pass `parent` + `root` refs when composing from a post thread 11 + - [ ] Drift migration: add `drafts` table (id, account_did, text, reply_uri, embed_json, media_paths, created_at, updated_at, scheduled_at) 12 + - [ ] Draft save on network failure, explicit save, and back-navigation 13 + - [ ] Drafts list UI accessible from compose toolbar 14 + - [ ] Scheduled posts — date/time picker, background task via WorkManager / BGTaskScheduler 15 + - [ ] Floating action button on home screen to open compose modal 16 + 17 + ## M9 — Notifications 18 + 19 + - [ ] Notifications screen with grouped-by-day notification list 20 + - [ ] `NotificationBloc` — events: `NotificationsRequested`, `NotificationsRefreshed`, `NotificationsPageLoaded`, `NotificationsMarkedRead` 21 + - [ ] Fetch notifications via `listNotifications` with cursor pagination 22 + - [ ] Render all notification reasons: like, repost, follow, mention, reply, quote 23 + - [ ] Each notification row: author avatar, reason icon, summary text, optional post preview 24 + - [ ] Unread count badge on nav bar via `getUnreadCount` polling (30s interval) 25 + - [ ] Mark as read via `updateSeen` when notifications screen opens 26 + - [ ] Tap notification to navigate to relevant post or profile 27 + 28 + ## M10 — Direct Messages 29 + 30 + - [ ] Conversation list screen via `chat.bsky.convo.listConvos` with pagination 31 + - [ ] `ConvoListBloc` — events: `ConvosRequested`, `ConvosRefreshed`, `ConvoMuted`, `ConvoUnmuted` 32 + - [ ] Primary / Requests tab filtering on conversation list 33 + - [ ] Message thread screen via `chat.bsky.convo.getMessages` with pagination 34 + - [ ] `MessageBloc` — events: `MessagesRequested`, `MessagesPageLoaded`, `MessageSent`, `MessageDeleted`, `ConvoMarkedRead` 35 + - [ ] Chat bubble layout — current user right-aligned, others left-aligned 36 + - [ ] Send messages via `chat.bsky.convo.sendMessage` 37 + - [ ] New conversation via `chat.bsky.convo.getConvoForMembers` 38 + - [ ] Long-press to copy individual messages, overflow menu "Copy All" for full thread 39 + - [ ] Mute / unmute conversations 40 + - [ ] Mark conversation as read via `chat.bsky.convo.updateRead` 41 + 42 + ## M11 — Account Switching 43 + 44 + - [ ] `AccountSwitcherCubit` exposing account list and active DID 45 + - [ ] Account switcher bottom sheet UI — list accounts with avatars and handles 46 + - [ ] Store `active_account_did` in Drift `settings` table 47 + - [ ] Drift migration: add `account_did` column to `cached_posts` if not present 48 + - [ ] All user-scoped queries filter by active account DID 49 + - [ ] Broadcast `AccountSwitched` event to all Blocs on switch 50 + - [ ] "Add Account" button triggers OAuth flow, inserts new `accounts` row 51 + - [ ] Silent token refresh on account switch; navigate to login on failure 52 + 53 + ## M12 — Offline Reading & Network Resilience 54 + 55 + - [ ] `ConnectivityCubit` via **connectivity_plus** — expose network state stream 56 + - [ ] Cache last-fetched feed page as serialised JSON in Drift 57 + - [ ] Display cached data immediately on launch, refresh in background 58 + - [ ] "You're offline" banner when connectivity is lost 59 + - [ ] Disable network-dependent actions (compose, like, repost, follow) when offline with tooltip 60 + - [ ] Notifications and DM screens show "No connection" empty state when offline with no cache 61 + 62 + ## M13 — Saved Posts 63 + 64 + - [ ] Drift migration: add `saved_posts` table (id, account_did, post_uri, post_json, saved_at) with unique constraint on (account_did, post_uri) 65 + - [ ] `SavedPostsCubit` — read/write saved posts, expose stream of saved URIs for icon state 66 + - [ ] Bookmark icon on post action bar — toggle saved state 67 + - [ ] Saved posts list screen accessible from profile or settings 68 + 69 + ## M14 — Jump to Profile 70 + 71 + - [ ] Floating action button on search screen 72 + - [ ] Handle input dialog with autocomplete via `searchActorsTypeahead` 73 + - [ ] Navigate to profile screen on selection or enter 74 + - [ ] Update bottom navigation to include Notifications and Messages tabs (5-tab layout)