wireweave 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/package.json +52 -0
- package/src/auth.js +97 -0
- package/src/debug.js +20 -0
- package/src/index.js +3 -0
- package/src/legacy/README.md +71 -0
- package/src/legacy/index.js +49 -0
- package/src/legacy/nostr-auth.js +217 -0
- package/src/legacy/nostr-bans.js +59 -0
- package/src/legacy/nostr-channels.js +145 -0
- package/src/legacy/nostr-chat.js +185 -0
- package/src/legacy/nostr-fsm.js +5 -0
- package/src/legacy/nostr-media.js +97 -0
- package/src/legacy/nostr-message.js +12 -0
- package/src/legacy/nostr-network.js +182 -0
- package/src/legacy/nostr-pages.js +152 -0
- package/src/legacy/nostr-roles.js +74 -0
- package/src/legacy/nostr-servers.js +223 -0
- package/src/legacy/nostr-settings.js +97 -0
- package/src/legacy/nostr-state-patch.js +19 -0
- package/src/legacy/nostr-voice-camera.js +63 -0
- package/src/legacy/nostr-voice-rtc.js +199 -0
- package/src/legacy/nostr-voice-sfu.js +128 -0
- package/src/legacy/nostr-voice.js +257 -0
- package/src/relay-pool.js +170 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
var serverPages = {
|
|
2
|
+
_store: new Map(),
|
|
3
|
+
_subs: new Map(),
|
|
4
|
+
|
|
5
|
+
_sanitize(html) {
|
|
6
|
+
const el = document.createElement('div');
|
|
7
|
+
el.innerHTML = html;
|
|
8
|
+
el.querySelectorAll('script,iframe,object,embed,form,input,button').forEach(n => n.remove());
|
|
9
|
+
el.querySelectorAll('*').forEach(n => {
|
|
10
|
+
[...n.attributes].forEach(a => {
|
|
11
|
+
if (/^on/i.test(a.name) || (a.name === 'href' && /^javascript:/i.test(a.value)) || (a.name === 'src' && /^javascript:/i.test(a.value))) n.removeAttribute(a.name);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
return el.innerHTML;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
_pageKey(serverId, slug) { return 'zellous-page:' + serverId + ':' + slug; },
|
|
18
|
+
|
|
19
|
+
getPages(serverId) { return Array.from((serverPages._store.get(serverId) || new Map()).values()); },
|
|
20
|
+
|
|
21
|
+
subscribe(serverId) {
|
|
22
|
+
if (serverPages._subs.has(serverId)) return;
|
|
23
|
+
var creator = serverId ? serverId.split(':')[0] : null;
|
|
24
|
+
if (!creator) return;
|
|
25
|
+
var subId = 'pages-' + serverId;
|
|
26
|
+
serverPages._subs.set(serverId, subId);
|
|
27
|
+
nostrNet.subscribe(subId,
|
|
28
|
+
[{ kinds: [30078], authors: [creator] }],
|
|
29
|
+
function(event) {
|
|
30
|
+
if (event.pubkey !== creator) return;
|
|
31
|
+
var dTag = (event.tags.find(function(t) { return t[0] === 'd'; }) || [])[1] || '';
|
|
32
|
+
var prefix = 'zellous-page:' + serverId + ':';
|
|
33
|
+
if (!dTag.startsWith(prefix)) return;
|
|
34
|
+
var slug = dTag.slice(prefix.length);
|
|
35
|
+
if (!slug) return;
|
|
36
|
+
try {
|
|
37
|
+
var data = JSON.parse(event.content);
|
|
38
|
+
var pages = serverPages._store.get(serverId) || new Map();
|
|
39
|
+
if (data.deleted) { pages.delete(slug); } else {
|
|
40
|
+
pages.set(slug, { slug: slug, title: data.title || slug, html: serverPages._sanitize(data.html || ''), updatedAt: event.created_at });
|
|
41
|
+
}
|
|
42
|
+
serverPages._store.set(serverId, pages);
|
|
43
|
+
if (window.uiChannels) uiChannels.render();
|
|
44
|
+
} catch(e) {}
|
|
45
|
+
},
|
|
46
|
+
function() {}
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
unsubscribe(serverId) {
|
|
51
|
+
var subId = serverPages._subs.get(serverId);
|
|
52
|
+
if (subId) { nostrNet.unsubscribe(subId); serverPages._subs.delete(serverId); }
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async publish(serverId, slug, title, html) {
|
|
56
|
+
if (!serverRoles.isAdmin(serverId)) throw new Error('Admin only');
|
|
57
|
+
var safe = serverPages._sanitize(html);
|
|
58
|
+
var dTag = serverPages._pageKey(serverId, slug);
|
|
59
|
+
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify({ title: title, html: safe }) });
|
|
60
|
+
await nostrNet.publish(signed);
|
|
61
|
+
var pages = serverPages._store.get(serverId) || new Map();
|
|
62
|
+
pages.set(slug, { slug: slug, title: title, html: safe, updatedAt: Math.floor(Date.now() / 1000) });
|
|
63
|
+
serverPages._store.set(serverId, pages);
|
|
64
|
+
if (window.uiChannels) uiChannels.render();
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async deletePage(serverId, slug) {
|
|
68
|
+
if (!serverRoles.isAdmin(serverId)) throw new Error('Admin only');
|
|
69
|
+
var dTag = serverPages._pageKey(serverId, slug);
|
|
70
|
+
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify({ deleted: true }) });
|
|
71
|
+
await nostrNet.publish(signed);
|
|
72
|
+
var pages = serverPages._store.get(serverId) || new Map();
|
|
73
|
+
pages.delete(slug);
|
|
74
|
+
serverPages._store.set(serverId, pages);
|
|
75
|
+
if (window.uiChannels) uiChannels.render();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
showEditModal(serverId, existing) {
|
|
79
|
+
document.getElementById('pageEditModal')?.remove();
|
|
80
|
+
var modal = document.createElement('div');
|
|
81
|
+
modal.id = 'pageEditModal';
|
|
82
|
+
modal.className = 'modal-overlay';
|
|
83
|
+
modal.style.display = 'flex';
|
|
84
|
+
var slug = existing ? existing.slug : 'page-' + Date.now().toString(36);
|
|
85
|
+
modal.innerHTML = '<div class="modal" style="max-width:680px;width:95%">' +
|
|
86
|
+
'<div class="modal-header"><span class="modal-title">' + (existing ? 'Edit Page' : 'New Page') + '</span>' +
|
|
87
|
+
'<button class="modal-close" id="pageModalClose">×</button></div>' +
|
|
88
|
+
'<div class="modal-body" style="display:flex;flex-direction:column;gap:12px">' +
|
|
89
|
+
'<input id="pageModalTitle" class="modal-input" placeholder="Page title" value="' + escHtml(existing ? existing.title : '') + '">' +
|
|
90
|
+
'<div id="pageModalEditor" class="page-editor" contenteditable="true" style="min-height:220px;background:var(--bg-raised);border:1px solid var(--border);border-radius:6px;padding:12px;overflow-y:auto;font-size:14px;line-height:1.6"></div>' +
|
|
91
|
+
'<div class="page-editor-toolbar" style="display:flex;gap:6px;flex-wrap:wrap">' +
|
|
92
|
+
'<button class="page-tb-btn" data-cmd="bold"><b>B</b></button>' +
|
|
93
|
+
'<button class="page-tb-btn" data-cmd="italic"><i>I</i></button>' +
|
|
94
|
+
'<button class="page-tb-btn" data-cmd="underline"><u>U</u></button>' +
|
|
95
|
+
'<button class="page-tb-btn" data-cmd="insertUnorderedList">•</button>' +
|
|
96
|
+
'<button class="page-tb-btn" data-cmd="insertOrderedList">1.</button>' +
|
|
97
|
+
'</div>' +
|
|
98
|
+
'</div>' +
|
|
99
|
+
'<div class="modal-footer">' +
|
|
100
|
+
(existing ? '<button class="modal-btn danger" id="pageModalDelete">Delete</button>' : '') +
|
|
101
|
+
'<button class="modal-btn secondary" id="pageModalCancel">Cancel</button>' +
|
|
102
|
+
'<button class="modal-btn primary" id="pageModalSave">Save</button>' +
|
|
103
|
+
'</div></div>';
|
|
104
|
+
document.body.appendChild(modal);
|
|
105
|
+
var editor = document.getElementById('pageModalEditor');
|
|
106
|
+
if (existing) editor.innerHTML = existing.html;
|
|
107
|
+
modal.querySelectorAll('.page-tb-btn').forEach(function(btn) {
|
|
108
|
+
btn.addEventListener('mousedown', function(e) { e.preventDefault(); document.execCommand(btn.dataset.cmd, false, null); editor.focus(); });
|
|
109
|
+
});
|
|
110
|
+
document.getElementById('pageModalClose').addEventListener('click', function() { modal.remove(); });
|
|
111
|
+
document.getElementById('pageModalCancel').addEventListener('click', function() { modal.remove(); });
|
|
112
|
+
if (existing) {
|
|
113
|
+
document.getElementById('pageModalDelete').addEventListener('click', async function() {
|
|
114
|
+
if (!confirm('Delete this page?')) return;
|
|
115
|
+
try { await serverPages.deletePage(serverId, slug); modal.remove(); } catch(e) { alert(e.message); }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
document.getElementById('pageModalSave').addEventListener('click', async function() {
|
|
119
|
+
var title = document.getElementById('pageModalTitle').value.trim();
|
|
120
|
+
if (!title) { alert('Title required'); return; }
|
|
121
|
+
try { await serverPages.publish(serverId, slug, title, editor.innerHTML); modal.remove(); } catch(e) { alert(e.message); }
|
|
122
|
+
});
|
|
123
|
+
modal.addEventListener('click', function(e) { if (e.target === modal) modal.remove(); });
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
renderPageView(serverId, slug) {
|
|
127
|
+
var pages = serverPages._store.get(serverId) || new Map();
|
|
128
|
+
var page = pages.get(slug);
|
|
129
|
+
var area = document.getElementById('pageView');
|
|
130
|
+
if (!area) return;
|
|
131
|
+
var isAdmin = window.serverRoles && serverRoles.isAdmin(serverId);
|
|
132
|
+
area.innerHTML = '<div class="page-view-header">' +
|
|
133
|
+
'<span class="page-view-title">' + escHtml(page ? page.title : slug) + '</span>' +
|
|
134
|
+
(isAdmin ? '<button class="modal-btn secondary page-edit-btn" id="pageEditBtn">Edit</button>' : '') +
|
|
135
|
+
'</div>' +
|
|
136
|
+
'<div class="page-view-body">' + (page ? page.html : '<p style="color:var(--text-muted)">No content yet.</p>') + '</div>';
|
|
137
|
+
if (isAdmin) {
|
|
138
|
+
document.getElementById('pageEditBtn').addEventListener('click', function() {
|
|
139
|
+
serverPages.showEditModal(serverId, page || { slug: slug, title: slug, html: '' });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
window.__zellous.pages = serverPages;
|
|
146
|
+
window.serverPages = serverPages;
|
|
147
|
+
if (!window.__debug) window.__debug = {};
|
|
148
|
+
Object.defineProperty(window.__debug, 'pages', { get: function() {
|
|
149
|
+
var out = {};
|
|
150
|
+
serverPages._store.forEach(function(pages, srv) { out[srv] = Array.from(pages.keys()); });
|
|
151
|
+
return out;
|
|
152
|
+
}, configurable: true });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
var serverRoles = {
|
|
2
|
+
_store: new Map(),
|
|
3
|
+
_sub: null,
|
|
4
|
+
|
|
5
|
+
_creatorOf(serverId) {
|
|
6
|
+
return serverId ? serverId.split(':')[0] : null;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
isOwner(serverId) {
|
|
10
|
+
return !!state.nostrPubkey && serverRoles._creatorOf(serverId) === state.nostrPubkey;
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
isAdmin(serverId) {
|
|
14
|
+
if (serverRoles.isOwner(serverId)) return true;
|
|
15
|
+
var roles = serverRoles._store.get(serverId) || {};
|
|
16
|
+
return !!(roles.admins || []).includes(state.nostrPubkey);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
isMod(serverId) {
|
|
20
|
+
if (serverRoles.isAdmin(serverId)) return true;
|
|
21
|
+
var roles = serverRoles._store.get(serverId) || {};
|
|
22
|
+
return !!(roles.mods || []).includes(state.nostrPubkey);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
getRole(serverId, pubkey) {
|
|
26
|
+
var creator = serverRoles._creatorOf(serverId);
|
|
27
|
+
if (pubkey === creator) return 'owner';
|
|
28
|
+
var roles = serverRoles._store.get(serverId) || {};
|
|
29
|
+
if ((roles.admins || []).includes(pubkey)) return 'admin';
|
|
30
|
+
if ((roles.mods || []).includes(pubkey)) return 'moderator';
|
|
31
|
+
return 'member';
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async setRole(serverId, targetPubkey, role) {
|
|
35
|
+
if (!serverRoles.isOwner(serverId) && role === 'admin') throw new Error('Only owner can assign admin');
|
|
36
|
+
if (!serverRoles.isAdmin(serverId)) throw new Error('Insufficient permissions');
|
|
37
|
+
var existing = serverRoles._store.get(serverId) || { admins: [], mods: [] };
|
|
38
|
+
var admins = (existing.admins || []).filter(function(p) { return p !== targetPubkey; });
|
|
39
|
+
var mods = (existing.mods || []).filter(function(p) { return p !== targetPubkey; });
|
|
40
|
+
if (role === 'admin') admins = admins.concat([targetPubkey]);
|
|
41
|
+
else if (role === 'moderator') mods = mods.concat([targetPubkey]);
|
|
42
|
+
var next = { admins: admins, mods: mods };
|
|
43
|
+
serverRoles._store.set(serverId, next);
|
|
44
|
+
var dTag = 'zellous-roles:' + serverId;
|
|
45
|
+
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify(next) });
|
|
46
|
+
await nostrNet.publish(signed);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
subscribe(serverId) {
|
|
50
|
+
if (serverRoles._sub) { nostrNet.unsubscribe(serverRoles._sub); serverRoles._sub = null; }
|
|
51
|
+
if (!serverId) return;
|
|
52
|
+
var creator = serverRoles._creatorOf(serverId);
|
|
53
|
+
if (!creator) return;
|
|
54
|
+
var dTag = 'zellous-roles:' + serverId;
|
|
55
|
+
serverRoles._sub = 'roles-' + serverId;
|
|
56
|
+
nostrNet.subscribe(serverRoles._sub,
|
|
57
|
+
[{ kinds: [30078], authors: [creator], '#d': [dTag] }],
|
|
58
|
+
function(event) {
|
|
59
|
+
if (event.pubkey !== creator) return;
|
|
60
|
+
try {
|
|
61
|
+
var data = JSON.parse(event.content);
|
|
62
|
+
serverRoles._store.set(serverId, { admins: data.admins || [], mods: data.mods || [] });
|
|
63
|
+
if (window.uiMembers) uiMembers.render();
|
|
64
|
+
} catch(e) {}
|
|
65
|
+
},
|
|
66
|
+
function() {}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
window.__zellous.roles = serverRoles;
|
|
72
|
+
window.serverRoles = serverRoles;
|
|
73
|
+
if (!window.__debug) window.__debug = {};
|
|
74
|
+
Object.defineProperty(window.__debug, 'roles', { get: function() { return { store: Object.fromEntries(serverRoles._store), sub: serverRoles._sub }; }, configurable: true });
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
var serverManager = {
|
|
2
|
+
loadServers: function() {
|
|
3
|
+
try {
|
|
4
|
+
var raw = localStorage.getItem('zn_servers');
|
|
5
|
+
state.servers = raw ? JSON.parse(raw) : [];
|
|
6
|
+
} catch (e) { state.servers = []; }
|
|
7
|
+
|
|
8
|
+
if (state.nostrPubkey) {
|
|
9
|
+
nostrNet.subscribe(
|
|
10
|
+
'my-servers',
|
|
11
|
+
[{ kinds: [34550], authors: [state.nostrPubkey] }],
|
|
12
|
+
function(event) { serverManager._handleServerEvent(event); },
|
|
13
|
+
function() {}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
var joined = localStorage.getItem('zn_joined_servers');
|
|
19
|
+
var joinedIds = joined ? JSON.parse(joined) : [];
|
|
20
|
+
joinedIds.forEach(function(sid) {
|
|
21
|
+
if (!state.servers.find(function(s) { return s.id === sid; })) {
|
|
22
|
+
state.servers = state.servers.concat([{ id: sid, name: sid.slice(0, 8), iconColor: '#5865F2' }]);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
// Subscribe to relay to fetch real names for joined (non-owned) servers
|
|
26
|
+
var unresolved = joinedIds.filter(function(sid) {
|
|
27
|
+
// Format: pubkey:dTag — only subscribe if not already owned by us
|
|
28
|
+
var parts = sid.split(':');
|
|
29
|
+
return parts.length === 2 && parts[0] !== state.nostrPubkey;
|
|
30
|
+
});
|
|
31
|
+
if (unresolved.length) {
|
|
32
|
+
// Group by author to minimise subscriptions
|
|
33
|
+
var byAuthor = {};
|
|
34
|
+
unresolved.forEach(function(sid) {
|
|
35
|
+
var parts = sid.split(':');
|
|
36
|
+
var author = parts[0], dTag = parts[1];
|
|
37
|
+
if (!byAuthor[author]) byAuthor[author] = [];
|
|
38
|
+
byAuthor[author].push(dTag);
|
|
39
|
+
});
|
|
40
|
+
Object.keys(byAuthor).forEach(function(author) {
|
|
41
|
+
var dTags = byAuthor[author];
|
|
42
|
+
nostrNet.subscribe(
|
|
43
|
+
'joined-server-' + author.slice(0, 8),
|
|
44
|
+
[{ kinds: [34550], authors: [author], '#d': dTags }],
|
|
45
|
+
function(event) { serverManager._handleServerEvent(event); },
|
|
46
|
+
function() {}
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
_handleServerEvent: function(event) {
|
|
54
|
+
var nameTag = event.tags.find(function(t) { return t[0] === 'name'; });
|
|
55
|
+
var colorTag = event.tags.find(function(t) { return t[0] === 'color'; });
|
|
56
|
+
var dTag = event.tags.find(function(t) { return t[0] === 'd'; });
|
|
57
|
+
if (!dTag) return;
|
|
58
|
+
var serverId = event.pubkey + ':' + dTag[1];
|
|
59
|
+
var name = nameTag ? nameTag[1] : serverId.slice(0, 8);
|
|
60
|
+
var iconColor = colorTag ? colorTag[1] : '#5865F2';
|
|
61
|
+
var existing = state.servers.find(function(s) { return s.id === serverId; });
|
|
62
|
+
if (existing) {
|
|
63
|
+
existing.name = name; existing.iconColor = iconColor; existing.ownerId = event.pubkey;
|
|
64
|
+
state.servers = state.servers.slice();
|
|
65
|
+
} else {
|
|
66
|
+
state.servers = state.servers.concat([{ id: serverId, name: name, iconColor: iconColor, ownerId: event.pubkey }]);
|
|
67
|
+
}
|
|
68
|
+
serverManager._persistServers();
|
|
69
|
+
ui.render.all();
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
create: async function(name, iconColor) {
|
|
73
|
+
var dTag = Math.random().toString(36).slice(2, 10);
|
|
74
|
+
var serverId = state.nostrPubkey + ':' + dTag;
|
|
75
|
+
var template = { kind: 34550, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag], ['name', name], ['color', iconColor || '#5865F2']], content: '' };
|
|
76
|
+
var signed = await auth.sign(template);
|
|
77
|
+
await nostrNet.publish(signed);
|
|
78
|
+
var server = { id: serverId, name: name, iconColor: iconColor || '#5865F2', ownerId: state.nostrPubkey };
|
|
79
|
+
state.servers = state.servers.concat([server]);
|
|
80
|
+
serverManager._persistServers();
|
|
81
|
+
serverManager.switchTo(serverId);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
join: async function(serverId) {
|
|
85
|
+
try {
|
|
86
|
+
var joined = JSON.parse(localStorage.getItem('zn_joined_servers') || '[]');
|
|
87
|
+
if (!joined.includes(serverId)) { joined.push(serverId); localStorage.setItem('zn_joined_servers', JSON.stringify(joined)); }
|
|
88
|
+
} catch (e) {}
|
|
89
|
+
if (!state.servers.find(function(s) { return s.id === serverId; })) {
|
|
90
|
+
state.servers = state.servers.concat([{ id: serverId, name: serverId.slice(0, 8), iconColor: '#5865F2' }]);
|
|
91
|
+
serverManager._persistServers();
|
|
92
|
+
}
|
|
93
|
+
serverManager.switchTo(serverId);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
delete: async function(serverId) {
|
|
97
|
+
state.servers = (state.servers || []).filter(function(s) { return s.id !== serverId; });
|
|
98
|
+
serverManager._persistServers();
|
|
99
|
+
try {
|
|
100
|
+
var joined = JSON.parse(localStorage.getItem('zn_joined_servers') || '[]');
|
|
101
|
+
localStorage.setItem('zn_joined_servers', JSON.stringify(joined.filter(function(id) { return id !== serverId; })));
|
|
102
|
+
} catch (e) {}
|
|
103
|
+
if (state.currentServerId === serverId) serverManager.switchTo(null);
|
|
104
|
+
else ui.render.all();
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
leave: function(serverId) { serverManager.delete(serverId); },
|
|
108
|
+
switchTo: async function(serverId) {
|
|
109
|
+
state.currentServerId = serverId;
|
|
110
|
+
state.chatMessages = []; state.channels = []; state.categories = [];
|
|
111
|
+
if (window.serverRoles) serverRoles.subscribe(serverId);
|
|
112
|
+
if (window.serverSettings) serverSettings.subscribe(serverId);
|
|
113
|
+
if (window.serverPages) serverPages.subscribe(serverId);
|
|
114
|
+
if (serverId) localStorage.setItem('zn_lastServer', serverId);
|
|
115
|
+
else localStorage.removeItem('zn_lastServer');
|
|
116
|
+
if (window.channelManager && serverId) {
|
|
117
|
+
await new Promise(function(resolve) {
|
|
118
|
+
var done = false;
|
|
119
|
+
var finish = function() { if (done) return; done = true; if (!(state.channels || []).length) channelManager._setDefaults(); resolve(); };
|
|
120
|
+
channelManager.loadChannels(serverId, finish);
|
|
121
|
+
setTimeout(finish, 3000);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
var firstText = (state.channels || []).find(function(c) { return c.type === 'text'; });
|
|
125
|
+
if (firstText && state.currentChannelId !== firstText.id) {
|
|
126
|
+
state.currentChannelId = firstText.id; state.currentChannel = firstText;
|
|
127
|
+
if (window.chat) chat.loadHistory(firstText.id);
|
|
128
|
+
}
|
|
129
|
+
ui.render.all();
|
|
130
|
+
},
|
|
131
|
+
_persistServers: function() { localStorage.setItem('zn_servers', JSON.stringify(state.servers)); },
|
|
132
|
+
init: function() {
|
|
133
|
+
serverManager.loadServers();
|
|
134
|
+
var lastServer = localStorage.getItem('zn_lastServer');
|
|
135
|
+
if (lastServer && state.servers.find(function(s) { return s.id === lastServer; })) serverManager.switchTo(lastServer);
|
|
136
|
+
else if (state.servers.length) serverManager.switchTo(state.servers[0].id);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
_getOrder: function() { try { return JSON.parse(localStorage.getItem('zn_serverOrder') || '[]'); } catch(e) { return []; } },
|
|
140
|
+
_saveOrder: function(ids) { localStorage.setItem('zn_serverOrder', JSON.stringify(ids)); },
|
|
141
|
+
|
|
142
|
+
_sortedServers: function() {
|
|
143
|
+
var srvs = state.servers || [];
|
|
144
|
+
var order = serverManager._getOrder();
|
|
145
|
+
if (!order.length) return srvs;
|
|
146
|
+
var indexed = {};
|
|
147
|
+
order.forEach(function(id, i) { indexed[id] = i; });
|
|
148
|
+
return srvs.slice().sort(function(a, b) {
|
|
149
|
+
var ai = indexed[a.id] !== undefined ? indexed[a.id] : Infinity;
|
|
150
|
+
var bi = indexed[b.id] !== undefined ? indexed[b.id] : Infinity;
|
|
151
|
+
return ai - bi;
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
renderList: function() {
|
|
156
|
+
var container = document.getElementById('serverIcons');
|
|
157
|
+
if (!container) return;
|
|
158
|
+
var srvs = serverManager._sortedServers();
|
|
159
|
+
var current = state.currentServerId;
|
|
160
|
+
var currentServer = current && srvs.find(function(s) { return s.id === current; });
|
|
161
|
+
var headerEl = document.getElementById('serverHeaderName');
|
|
162
|
+
if (headerEl) headerEl.textContent = currentServer ? currentServer.name : 'Zellous';
|
|
163
|
+
var colors = ['#5865f2', '#57f287', '#feb347', '#fe7168', '#9b59b6', '#1abc9c', '#e67e22', '#e74c3c'];
|
|
164
|
+
var html = '';
|
|
165
|
+
srvs.forEach(function(s) {
|
|
166
|
+
var active = s.id === current ? ' active' : '';
|
|
167
|
+
var color = s.iconColor || colors[s.name.length % colors.length];
|
|
168
|
+
var initial = (s.name || '?')[0].toUpperCase();
|
|
169
|
+
html += '<div class="server-icon' + active + '" draggable="true" data-server="' + s.id + '" title="' + s.name + '" style="background:' + (active ? '' : color) + '">' +
|
|
170
|
+
'<div class="server-pill"></div>' + initial + '</div>';
|
|
171
|
+
});
|
|
172
|
+
html += '<div class="server-separator" id="serverSeparator"></div>' +
|
|
173
|
+
'<div class="server-icon add-server" id="addServerBtn" title="Add a Server"><div class="server-pill"></div>+</div>';
|
|
174
|
+
container.innerHTML = html;
|
|
175
|
+
var homeIcon = document.getElementById('homeServer');
|
|
176
|
+
if (homeIcon) {
|
|
177
|
+
homeIcon.classList.toggle('active', !current);
|
|
178
|
+
homeIcon.addEventListener('click', function() { serverManager.switchTo(null); });
|
|
179
|
+
}
|
|
180
|
+
container.querySelectorAll('[data-server]').forEach(function(el) {
|
|
181
|
+
el.addEventListener('click', function() { serverManager.switchTo(el.dataset.server); });
|
|
182
|
+
el.addEventListener('contextmenu', function(e) { e.preventDefault(); serverManager.showContextMenu(el.dataset.server, e.clientX, e.clientY); });
|
|
183
|
+
});
|
|
184
|
+
var addBtn = document.getElementById('addServerBtn');
|
|
185
|
+
if (addBtn) addBtn.addEventListener('click', function() { serverManager.showCreateModal(); });
|
|
186
|
+
serverManager._initDragDrop(container);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
_initDragDrop: function(container) {
|
|
190
|
+
var dragId = null;
|
|
191
|
+
container.addEventListener('dragstart', function(e) {
|
|
192
|
+
var el = e.target.closest('[data-server]');
|
|
193
|
+
if (!el) return;
|
|
194
|
+
dragId = el.dataset.server; e.dataTransfer.effectAllowed = 'move';
|
|
195
|
+
setTimeout(function() { el.classList.add('dragging'); }, 0);
|
|
196
|
+
});
|
|
197
|
+
container.addEventListener('dragend', function() {
|
|
198
|
+
container.querySelectorAll('.dragging').forEach(function(el) { el.classList.remove('dragging'); });
|
|
199
|
+
container.querySelectorAll('.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
|
|
200
|
+
dragId = null;
|
|
201
|
+
});
|
|
202
|
+
container.addEventListener('dragover', function(e) {
|
|
203
|
+
e.preventDefault(); e.dataTransfer.dropEffect = 'move';
|
|
204
|
+
var target = e.target.closest('[data-server]');
|
|
205
|
+
container.querySelectorAll('.drag-over').forEach(function(el) { el.classList.remove('drag-over'); });
|
|
206
|
+
if (target && target.dataset.server !== dragId) target.classList.add('drag-over');
|
|
207
|
+
});
|
|
208
|
+
container.addEventListener('drop', function(e) {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
var target = e.target.closest('[data-server]');
|
|
211
|
+
if (!target || !dragId || target.dataset.server === dragId) return;
|
|
212
|
+
var srvs = serverManager._sortedServers();
|
|
213
|
+
var ids = srvs.map(function(s) { return s.id; });
|
|
214
|
+
var fromIdx = ids.indexOf(dragId), toIdx = ids.indexOf(target.dataset.server);
|
|
215
|
+
if (fromIdx === -1 || toIdx === -1) return;
|
|
216
|
+
ids.splice(fromIdx, 1); ids.splice(toIdx, 0, dragId);
|
|
217
|
+
serverManager._saveOrder(ids); serverManager.renderList();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
window.__zellous.servers = serverManager;
|
|
223
|
+
window.serverManager = serverManager;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var serverSettings = {
|
|
2
|
+
_store: new Map(),
|
|
3
|
+
_sub: null,
|
|
4
|
+
_VALID_BITRATES: [8000, 16000, 24000, 48000, 96000],
|
|
5
|
+
|
|
6
|
+
_clampBitrate(v) {
|
|
7
|
+
return serverSettings._VALID_BITRATES.reduce(function(prev, cur) {
|
|
8
|
+
return Math.abs(cur - v) < Math.abs(prev - v) ? cur : prev;
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
getBitrate(serverId) {
|
|
13
|
+
var s = serverSettings._store.get(serverId);
|
|
14
|
+
return (s && s.opusBitrate) || 24000;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async setBitrate(serverId, bitrate) {
|
|
18
|
+
if (!serverRoles.isOwner(serverId) && !serverRoles.isAdmin(serverId)) throw new Error('Insufficient permissions');
|
|
19
|
+
var clamped = serverSettings._clampBitrate(Number(bitrate));
|
|
20
|
+
var existing = serverSettings._store.get(serverId) || {};
|
|
21
|
+
var next = Object.assign({}, existing, { opusBitrate: clamped });
|
|
22
|
+
serverSettings._store.set(serverId, next);
|
|
23
|
+
var dTag = 'zellous-settings:' + serverId;
|
|
24
|
+
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify(next) });
|
|
25
|
+
await nostrNet.publish(signed);
|
|
26
|
+
if (state.audioEncoder) {
|
|
27
|
+
try { state.audioEncoder.configure({ codec: 'opus', sampleRate: config.sampleRate, numberOfChannels: 1, bitrate: clamped }); } catch(e) {}
|
|
28
|
+
}
|
|
29
|
+
return clamped;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getEmbedAllowlist(serverId) {
|
|
33
|
+
var s = serverSettings._store.get(serverId);
|
|
34
|
+
return (s && s.embedAllowlist) ? (typeof s.embedAllowlist === 'string' ? s.embedAllowlist.split(',').map(function(d) { return d.trim(); }).filter(Boolean) : s.embedAllowlist) : [];
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async setEmbedAllowlist(serverId, domainsStr) {
|
|
38
|
+
if (!serverRoles.isOwner(serverId) && !serverRoles.isAdmin(serverId)) throw new Error('Insufficient permissions');
|
|
39
|
+
var domains = domainsStr.split(',').map(function(d) { return d.trim(); }).filter(Boolean);
|
|
40
|
+
var existing = serverSettings._store.get(serverId) || {};
|
|
41
|
+
var next = Object.assign({}, existing, { embedAllowlist: domains });
|
|
42
|
+
serverSettings._store.set(serverId, next);
|
|
43
|
+
var dTag = 'zellous-settings:' + serverId;
|
|
44
|
+
var signed = await auth.sign({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dTag]], content: JSON.stringify(next) });
|
|
45
|
+
await nostrNet.publish(signed);
|
|
46
|
+
return domains;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
isOriginAllowed(serverId, origin) {
|
|
50
|
+
if (!origin) return false;
|
|
51
|
+
var allowlist = serverSettings.getEmbedAllowlist(serverId);
|
|
52
|
+
if (allowlist.length === 0) return true;
|
|
53
|
+
var url = {};
|
|
54
|
+
try { url = new URL(origin); } catch(e) { return false; }
|
|
55
|
+
var hostname = url.hostname;
|
|
56
|
+
return allowlist.some(function(pattern) {
|
|
57
|
+
if (pattern === '*') return true;
|
|
58
|
+
if (pattern.startsWith('*.')) {
|
|
59
|
+
var suffix = pattern.slice(2);
|
|
60
|
+
return hostname === suffix || hostname.endsWith('.' + suffix);
|
|
61
|
+
}
|
|
62
|
+
return hostname === pattern || hostname === pattern.replace('https://', '').replace('http://', '');
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
subscribe(serverId) {
|
|
67
|
+
if (serverSettings._sub) { nostrNet.unsubscribe(serverSettings._sub); serverSettings._sub = null; }
|
|
68
|
+
if (!serverId) return;
|
|
69
|
+
var creator = serverId.split(':')[0];
|
|
70
|
+
if (!creator) return;
|
|
71
|
+
var dTag = 'zellous-settings:' + serverId;
|
|
72
|
+
serverSettings._sub = 'settings-' + serverId;
|
|
73
|
+
nostrNet.subscribe(serverSettings._sub,
|
|
74
|
+
[{ kinds: [30078], authors: [creator], '#d': [dTag] }],
|
|
75
|
+
function(event) {
|
|
76
|
+
if (event.pubkey !== creator) return;
|
|
77
|
+
try {
|
|
78
|
+
var data = JSON.parse(event.content);
|
|
79
|
+
serverSettings._store.set(serverId, data);
|
|
80
|
+
} catch(e) {}
|
|
81
|
+
},
|
|
82
|
+
function() {}
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
applyToEncoder() {
|
|
87
|
+
var bitrate = serverSettings.getBitrate(state.currentServerId);
|
|
88
|
+
if (state.audioEncoder) {
|
|
89
|
+
try { state.audioEncoder.configure({ codec: 'opus', sampleRate: config.sampleRate, numberOfChannels: 1, bitrate: bitrate }); } catch(e) {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
window.__zellous.settings = serverSettings;
|
|
95
|
+
window.serverSettings = serverSettings;
|
|
96
|
+
if (!window.__debug) window.__debug = {};
|
|
97
|
+
Object.defineProperty(window.__debug, 'settings', { get: function() { return { store: Object.fromEntries(serverSettings._store), sub: serverSettings._sub }; }, configurable: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const sig = window.__signal;
|
|
3
|
+
const raw = window.stateSignals;
|
|
4
|
+
|
|
5
|
+
raw.nostrPubkey = sig('');
|
|
6
|
+
raw.nostrPrivkey = sig(null);
|
|
7
|
+
raw.nostrRelays = sig([
|
|
8
|
+
'wss://relay.damus.io',
|
|
9
|
+
'wss://relay.primal.net',
|
|
10
|
+
'wss://relay.snort.social',
|
|
11
|
+
'wss://nos.lol',
|
|
12
|
+
'wss://relay.olas.app',
|
|
13
|
+
'wss://nostr.mom'
|
|
14
|
+
]);
|
|
15
|
+
raw.nostrRelayStatus = sig(new Map());
|
|
16
|
+
raw.nostrProfile = sig(null);
|
|
17
|
+
raw.nostrSubscriptions = sig(new Map());
|
|
18
|
+
raw.currentChannelId = sig(null);
|
|
19
|
+
})();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
var nostrVoiceCamera = {
|
|
2
|
+
_fsm: null,
|
|
3
|
+
|
|
4
|
+
_initFSM: function() {
|
|
5
|
+
var actor = XState.createActor(nostrFsm.cameraMachine);
|
|
6
|
+
actor.subscribe(function(snap) {
|
|
7
|
+
var state_val = snap.value;
|
|
8
|
+
document.getElementById('voiceCamBtn')?.classList.toggle('active', state_val === 'active');
|
|
9
|
+
});
|
|
10
|
+
actor.start();
|
|
11
|
+
nostrVoiceCamera._fsm = actor;
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
toggle() {
|
|
15
|
+
if (!nostrVoiceCamera._fsm) nostrVoiceCamera._initFSM();
|
|
16
|
+
var snap = nostrVoiceCamera._fsm.getSnapshot();
|
|
17
|
+
if (snap.matches('active')) {
|
|
18
|
+
nostrVoiceCamera.stop();
|
|
19
|
+
} else if (snap.matches('idle')) {
|
|
20
|
+
nostrVoiceCamera.start();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async start() {
|
|
25
|
+
if (!nostrVoiceCamera._fsm) nostrVoiceCamera._initFSM();
|
|
26
|
+
var snap = nostrVoiceCamera._fsm.getSnapshot();
|
|
27
|
+
if (!snap.can({type:'enable'})) return;
|
|
28
|
+
nostrVoiceCamera._fsm.send({type:'enable'});
|
|
29
|
+
state.cameraEnabled = true;
|
|
30
|
+
if (nostrVoice._cameraStream) {
|
|
31
|
+
var track = nostrVoice._cameraStream.getVideoTracks()[0];
|
|
32
|
+
nostrVoice._peers.forEach(function(peer) {
|
|
33
|
+
var sender = peer.pc.getSenders().find(function(s) { return s.track && s.track.kind === 'video'; });
|
|
34
|
+
if (sender) { sender.replaceTrack(track).catch(function(){}); }
|
|
35
|
+
else { peer.pc.addTrack(track, nostrVoice._cameraStream); }
|
|
36
|
+
});
|
|
37
|
+
var local = nostrVoice._participants.get('local');
|
|
38
|
+
if (local) local.hasVideo = true;
|
|
39
|
+
nostrVoiceCamera._fsm.send({type:'enabled'});
|
|
40
|
+
nostrVoice.updateParticipants();
|
|
41
|
+
} else {
|
|
42
|
+
nostrVoiceCamera._fsm.send({type:'denied'});
|
|
43
|
+
state.cameraEnabled = false;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
stop() {
|
|
48
|
+
if (!nostrVoiceCamera._fsm) nostrVoiceCamera._initFSM();
|
|
49
|
+
if (!nostrVoiceCamera._fsm.getSnapshot().can({type:'disable'})) return;
|
|
50
|
+
nostrVoiceCamera._fsm.send({type:'disable'});
|
|
51
|
+
state.cameraEnabled = false;
|
|
52
|
+
nostrVoice._peers.forEach(function(peer) {
|
|
53
|
+
var sender = peer.pc.getSenders().find(function(s) { return s.track && s.track.kind === 'video'; });
|
|
54
|
+
if (sender) { sender.replaceTrack(null).catch(function(){}); }
|
|
55
|
+
});
|
|
56
|
+
var local = nostrVoice._participants.get('local');
|
|
57
|
+
if (local) local.hasVideo = false;
|
|
58
|
+
nostrVoice.updateParticipants();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
window.__zellous.voiceCamera = nostrVoiceCamera;
|
|
63
|
+
window.nostrVoiceCamera = nostrVoiceCamera;
|