| … | … | @@ -251,6 +251,7 @@ button.liked{font-weight:700;border-color:var(--accent);color:var(--accent)} |
| 251 | 251 | |
| 252 | 252 | /* The composer is not the page. It was a third of the first screen before any content: a tall box, |
| 253 | 253 | a permanent file input and two lines of help that never changed. */ |
| 254 | .banner-lg{width:100%;max-height:190px;object-fit:cover;border-radius:10px;border:1px solid var(--line)} | |
| 254 | 255 | .composer{background:var(--bg-soft);border:1px solid var(--line);border-radius:12px; |
| 255 | 256 | padding:var(--s2);margin-bottom:var(--s4)} |
| 256 | 257 | .composer textarea{border:0;padding:var(--s1);min-height:3.2rem;resize:vertical} |
| … | … | @@ -1433,6 +1434,11 @@ export function settings (profile, message, handle = null, namespace = null, tok |
| 1433 | 1434 | const shown = p.avatar |
| 1434 | 1435 | ? '<p><img class="av-lg" src="/avatar/' + esc(p.avatar) + '" alt="your avatar"></p>' |
| 1435 | 1436 | : '<p class="who">No picture yet.</p>' |
| 1437 | // The banner is shown at its own width rather than in the avatar's circle — they are different | |
| 1438 | // pictures with different shapes, and previewing one as the other teaches the wrong thing. | |
| 1439 | const shownBanner = p.banner | |
| 1440 | ? '<p><img class="banner-lg" src="/avatar/' + esc(p.banner) + '" alt="your banner"></p>' | |
| 1441 | : '' | |
| 1436 | 1442 | return layout('settings', note + reachSettings(reach) + |
| 1437 | 1443 | '<h1>Settings</h1>' + shown + |
| 1438 | 1444 | '<form method="post" action="/settings" enctype="multipart/form-data">' + |
| … | … | @@ -1442,6 +1448,11 @@ export function settings (profile, message, handle = null, namespace = null, tok |
| 1442 | 1448 | '</label></p>' + |
| 1443 | 1449 | '<p class="who">png, jpeg, gif or webp, up to 512 KiB. The type is read from the file itself, ' + |
| 1444 | 1450 | 'not its name.</p>' + |
| 1451 | shownBanner + | |
| 1452 | '<p><label>Banner<br><input type="file" name="banner" accept="image/png,image/jpeg,image/gif,image/webp">' + | |
| 1453 | '</label></p>' + | |
| 1454 | '<p class="who">The wide header image. Same formats and cap. Leaving either file empty leaves ' + | |
| 1455 | 'that picture as it was.</p>' + | |
| 1445 | 1456 | // ⛔ The profile record has had a body slot from the start and nothing ever wrote it, so every |
| 1446 | 1457 | // screen that wanted to say who somebody IS had a name, a picture and 64 hex characters. |
| 1447 | 1458 | '<p><label>About you<br><textarea name="bio" rows="3" placeholder="a sentence or two — markdown">' + |
| … | … | @@ -757,11 +757,12 @@ export async function route (req, res, ctx) { |
| 757 | 757 | } |
| 758 | 758 | |
| 759 | 759 | if (req.method === 'POST' && path === '/settings') { |
| 760 | // 2 MiB: the avatar cap is 512 KiB, and the slack is so an oversized picture is refused by the | |
| 761 | // daemon with a message about its size rather than by the transport with nothing useful. | |
| 760 | // 3 MiB: each picture is capped at 512 KiB by the daemon and the form can now carry TWO, and | |
| 761 | // the slack is so an oversized picture is refused by the daemon with a message about its size | |
| 762 | // rather than by the transport with nothing useful. | |
| 762 | 763 | let form |
| 763 | 764 | try { |
| 764 | form = parseMultipart(await readBytes(req, 2 * 1024 * 1024), req.headers['content-type']) | |
| 765 | form = parseMultipart(await readBytes(req, 3 * 1024 * 1024), req.headers['content-type']) | |
| 765 | 766 | } catch (err) { |
| 766 | 767 | return seeOther(res, '/settings?m=' + encodeURIComponent(err.message)) |
| 767 | 768 | } |
| … | … | @@ -772,13 +773,17 @@ export async function route (req, res, ctx) { |
| 772 | 773 | if (typeof form.fields.name === 'string') set.name = form.fields.name |
| 773 | 774 | if (typeof form.fields.bio === 'string') set.bio = form.fields.bio |
| 774 | 775 | if (Object.keys(set).length) await client.call('set-profile', set) |
| 775 | const file = form.files.avatar | |
| 776 | if (file && file.bytes.length) { | |
| 776 | // ⛔ TWO PICTURES, one form. The avatar is the round one; the banner is the wide header. | |
| 777 | // They are independent — submitting the form with only one file set must not clear the other, | |
| 778 | // which is why each is only touched when its own part carries bytes. | |
| 779 | for (const [field, verb] of [['avatar', 'avatar-put'], ['banner', 'banner-put']]) { | |
| 780 | const file = form.files[field] | |
| 781 | if (!file || !file.bytes.length) continue | |
| 777 | 782 | // ⛔ A PATH, for the same reason. Written where the daemon can read it; it copies what it |
| 778 | 783 | // keeps, so this temp file is ours to remove. |
| 779 | 784 | const tmp = join(os.tmpdir(), 'favela-upload-' + Date.now() + '-' + Math.random().toString(16).slice(2)) |
| 780 | 785 | fs.writeFileSync(tmp, file.bytes) |
| 781 | try { await client.call('avatar-put', { path: tmp }) } finally { try { fs.unlinkSync(tmp) } catch {} } | |
| 786 | try { await client.call(verb, { path: tmp }) } finally { try { fs.unlinkSync(tmp) } catch {} } | |
| 782 | 787 | } |
| 783 | 788 | } catch (err) { |
| 784 | 789 | return seeOther(res, '/settings?m=' + encodeURIComponent(err.message)) |
| … | … | @@ -1,7 +1,7 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-surface", |
| 3 | "version": "0.0.47", | |
| 4 | "description": "The favela renderer — server-side HTML over the daemon's socket. No storage, no keys, no swarm.", | |
| 3 | "version": "0.0.48", | |
| 4 | "description": "The favela renderer \u2014 server-side HTML over the daemon's socket. No storage, no keys, no swarm.", | |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| 7 | 7 | "bin": { |