summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js65
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css47
3 files changed, 112 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index f9f6b55..fee21dc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -255,6 +255,8 @@ const ICON_PATHS = {
door: ['M13.5 3.5H6a1 1 0 0 0-1 1v15a1 1 0 0 0 1 1h7.5',
'M10.5 12H21', 'M17.8 8.8L21 12l-3.2 3.2'],
clip: ['M20.5 11.8l-8.4 8.4a5.4 5.4 0 0 1-7.6-7.6l8.8-8.8a3.6 3.6 0 0 1 5.1 5.1l-8.8 8.8a1.8 1.8 0 0 1-2.5-2.5l8.1-8.1'],
+ pencil: ['M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17v3',
+ 'M14.5 6.5l3 3'],
check: ['M4.5 12.5l5 5 10-11'],
chevron: ['M6 9.5l6 6 6-6'],
close: ['M6 6l12 12M18 6L6 18'],
@@ -871,11 +873,14 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk
}
function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
- onJoined }) {
+ onJoined, onGroupUpdated }) {
const [status, setStatus] = useState('idle');
const [entries, setEntries] = useState([]);
const [error, setError] = useState('');
+ const [editingDesc, setEditingDesc] = useState(false);
+ const [descDraft, setDescDraft] = useState('');
+ const [savingDesc, setSavingDesc] = useState(false);
const [sortKey, setSortKey] = useState('name');
const [sortAsc, setSortAsc] = useState(true);
const [filter, setFilter] = useState('');
@@ -1143,6 +1148,22 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
}
}, [currentPath]);
+ const saveDescription = useCallback(async (e) => {
+ e.preventDefault();
+ setSavingDesc(true);
+ try {
+ const r = await hubFetch(`/v1/groups/${groupId}`, {
+ method: 'PATCH', token, body: { description: descDraft },
+ });
+ if (onGroupUpdated) onGroupUpdated(groupId, { description: r.description });
+ setEditingDesc(false);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setSavingDesc(false);
+ }
+ }, [groupId, token, descDraft, onGroupUpdated]);
+
const deleteFile = useCallback(async (entry) => {
const transport = transportRef.current;
if (!transport || !transport.connected) return;
@@ -1233,9 +1254,35 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth,
<h2 style="margin-bottom:${group && group.description ? '4px' : '0'}">
${group ? group.name : t('group.default_name')}
</h2>
- ${group && group.description && html`
- <p class="group-desc">${group.description}</p>
- `}
+ ${editingDesc
+ ? html`
+ <form class="group-desc-edit" onSubmit=${saveDescription}>
+ <textarea rows="2" maxlength="512" autofocus
+ placeholder="${t('group.desc_placeholder')}"
+ value=${descDraft}
+ onInput=${e => setDescDraft(e.target.value)}></textarea>
+ <div>
+ <button class="admin-btn" type="submit" disabled=${savingDesc}>
+ ${savingDesc ? '...' : t('group.desc_save')}
+ </button>
+ <button class="btn-secondary" type="button"
+ onClick=${() => setEditingDesc(false)}>${t('group.desc_cancel')}</button>
+ </div>
+ </form>
+ `
+ : html`
+ ${group && group.description && html`
+ <p class="group-desc">${group.description}</p>
+ `}
+ ${group && group.is_admin && html`
+ <button class="link-btn" title=${t('group.desc_edit')}
+ onClick=${() => { setDescDraft(group.description || '');
+ setEditingDesc(true); }}>
+ <${Icon} name="pencil" />${' '}
+ ${group.description ? t('group.desc_edit') : t('group.desc_add')}
+ </button>
+ `}
+ `}
</div>
<span class="status-badge ${statusClass}">
${(status === 'discovering' || status === 'connecting' || status === 'fetching')
@@ -2903,6 +2950,13 @@ function App() {
fetchNotifications();
}, [user]);
+ // The group list lives here, so an edit made three components down has to come
+ // back up rather than be re-fetched: a reload would drop the WebRTC connection
+ // the page is holding.
+ const updateGroup = useCallback((gid, patch) => {
+ setGroups(prev => prev.map(g => (g.id === gid ? { ...g, ...patch } : g)));
+ }, []);
+
const markRead = useCallback((id) => {
if (!user) return;
// Drop it here and now. Waiting for the round trip leaves it on screen while
@@ -3011,7 +3065,8 @@ function App() {
page = html`<${GroupPage}
groupId=${groupId} group=${group} token=${user.token}
username=${user.username} userId=${user.userId}
- onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications} />`;
+ onRefreshAuth=${refreshAuth} onJoined=${dismissGroupNotifications}
+ onGroupUpdated=${updateGroup} />`;
} else if (route === '/admin') {
page = (user.role === 'moderator' || user.role === 'admin')
? html`<${AdminPage} token=${user.token} />`
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
index cca595d..a6281e2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
@@ -278,6 +278,11 @@ const en = {
+ 'you normally talk. It works once, and it never passes through the hub.',
'members.invite_code_hint': 'They enter it the first time they open this group. '
+ 'You do not need to be online then.',
+ 'group.desc_edit': 'Edit description',
+ 'group.desc_add': 'Add a description',
+ 'group.desc_save': 'Save',
+ 'group.desc_cancel': 'Cancel',
+ 'group.desc_placeholder': 'What this group is for — members see it on their home page.',
'group.join_code_title': 'This node needs to recognise you',
'group.join_code_hint': 'Ask whoever invited you for the one-time code, and enter '
+ 'it here. After that this browser is recognised and you will not be asked again.',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index fab4f55..f83b433 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -1157,6 +1157,53 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
.admin-btn.danger:hover { border-color: var(--error); }
.admin-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+/* Used in four places and never defined, so it rendered as a bare browser
+ button next to styled ones. Same shape as .admin-btn, quieter. */
+.btn-secondary {
+ padding: 4px 10px;
+ border: 1px solid transparent;
+ border-radius: 5px;
+ background: none;
+ color: var(--text-secondary);
+ font-size: 0.82em;
+ cursor: pointer;
+ white-space: nowrap;
+}
+.btn-secondary:hover { border-color: var(--border); color: var(--text); }
+.btn-secondary:disabled { opacity: 0.4; cursor: not-allowed; }
+
+/* Text that acts as a button: the "add a description" affordance should not
+ compete with the group's name for attention. */
+.link-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ padding: 0;
+ border: none;
+ background: none;
+ color: var(--text-dim);
+ font-size: 0.8em;
+ cursor: pointer;
+}
+.link-btn:hover { color: var(--accent); }
+
+/* The header is a flex row, so a textarea in it collapses to its content
+ width unless told otherwise. */
+.group-desc-edit { margin: 4px 0 6px; width: min(460px, 60vw); min-width: 260px; }
+.group-desc-edit textarea {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: var(--bg-surface);
+ color: var(--text);
+ font: inherit;
+ font-size: 0.85em;
+ resize: vertical;
+}
+.group-desc-edit textarea:focus { outline: none; border-color: var(--border-focus); }
+.group-desc-edit div { display: flex; gap: 6px; margin-top: 4px; }
+
.admin-role-select {
padding: 3px 6px;
border: 1px solid var(--border);