mirror of
https://github.com/aljazceru/nostr-profile-manager.git
synced 2025-12-17 05:34:20 +01:00
added relay form and backup history
This commit is contained in:
@@ -13,7 +13,7 @@ Supported profile events: kind `0`, `2`, `10002` and `3`.
|
||||
- [x] backup your profile events to offline browser storage
|
||||
- [x] review changes between backups
|
||||
- [x] `0`
|
||||
- [ ] `10002` and `2`
|
||||
- [x] `10002`
|
||||
- [x] `3`
|
||||
- [ ] selectively restore previous versions
|
||||
- [x] download profile backup history as JSON file
|
||||
@@ -35,8 +35,8 @@ Supported profile events: kind `0`, `2`, `10002` and `3`.
|
||||
- [ ] Contacts recommendation based off social graph
|
||||
- [ ] Suggest updates to contact relay based on Contact's kind `10002` and `2` events
|
||||
|
||||
- [ ] Relays
|
||||
- [ ] editable table of read / write relays kind `10002` event
|
||||
- [x] Relays
|
||||
- [x] editable table of read / write relays kind `10002` event
|
||||
- [ ] auto suggestion of `10002` event based on contact's relays if no event present
|
||||
- [ ] evaluation of `10002` based on contact's
|
||||
- [ ] decentralisation score to encourage users not to use the same relay
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
generateContactsChanges, generateHistoryTable, generateMetadataChanges, Kind3Event,
|
||||
generateContactsChanges, generateHistoryTable, generateMetadataChanges, generateRelayChanges,
|
||||
Kind10002Event, Kind3Event,
|
||||
} from './LoadHistory';
|
||||
import { MetadataFlex } from './LoadMetadataPage';
|
||||
import SampleEvents from './SampleEvents';
|
||||
@@ -124,3 +125,121 @@ describe('generateContactsChanges', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateRelaysChanges', () => {
|
||||
test('the oldest event list all the relays', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com'],
|
||||
['r', 'wss://brando-relay1.com'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual([
|
||||
'wss://alicerelay.example.com',
|
||||
'wss://brando-relay.com',
|
||||
'wss://brando-relay1.com',
|
||||
]);
|
||||
});
|
||||
test('read only and write only relays are maked as such', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
['r', 'wss://brando-relay1.com', 'write'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual([
|
||||
'wss://alicerelay.example.com',
|
||||
'wss://brando-relay.com read only',
|
||||
'wss://brando-relay1.com write only',
|
||||
]);
|
||||
});
|
||||
test('when a relay is added, the addition is listed in the changes array', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
['r', 'wss://brando-relay1.com', 'write'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual(['<div class="added">added <mark>wss://brando-relay1.com write only</mark></div>']);
|
||||
});
|
||||
test('when a relay is removed, the removal is listed in the changes array', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
['r', 'wss://brando-relay1.com', 'write'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual(['<div class="removed">removed <mark>wss://brando-relay1.com write only</mark></div>']);
|
||||
});
|
||||
test('when a relay is modified, the removal is listed in the changes array', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'write'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual(['<div class="modified">modified <mark>wss://brando-relay.com read only</mark></div>']);
|
||||
});
|
||||
test('when a contact is added and another removed, both events are listed in the changes array', () => {
|
||||
const r = generateRelayChanges([
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://brando-relay1.com', 'write'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
{
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com', 'read'],
|
||||
],
|
||||
} as Kind10002Event,
|
||||
]);
|
||||
expect(r[0].changes).toEqual([
|
||||
'<div class="added">added <mark>wss://brando-relay1.com write only</mark></div>',
|
||||
'<div class="removed">removed <mark>wss://alicerelay.example.com</mark></div>',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,11 +104,63 @@ export const generateContactsChanges = (
|
||||
};
|
||||
});
|
||||
|
||||
export type Kind10002Tag = ['r', string] | ['r', string, 'read' | 'write' ];
|
||||
|
||||
export interface Kind10002Event extends Event {
|
||||
kind:3;
|
||||
tags:Kind10002Tag[]
|
||||
}
|
||||
|
||||
const summariseRelay = (r: Kind10002Tag): string => r[1] + (r[2] ? ` ${r[2]} only` : '');
|
||||
|
||||
export const generateRelayChanges = (
|
||||
history: Kind10002Event[],
|
||||
):VersionChange[] => history.map((e, i, a) => {
|
||||
const changes:string[] = [];
|
||||
const current = e.tags.filter((t) => t[0] === 'r');
|
||||
// if first backup list all relays
|
||||
if (i === a.length - 1) e.tags.forEach((r) => changes.push(summariseRelay(r)));
|
||||
else {
|
||||
const next = a[i + 1].tags;
|
||||
// list adds
|
||||
const added = current.filter((c) => !next.some((n) => n[1] === c[1]));
|
||||
if (added.length > 0) {
|
||||
added.forEach((r) => changes.push(
|
||||
`<div class="added">added <mark>${summariseRelay(r)}</mark></div>`,
|
||||
));
|
||||
}
|
||||
// list modified
|
||||
const modified = current.filter(
|
||||
(c) => next.filter((n) => n[1] === c[1]).some((n) => c[2] !== n[2]),
|
||||
);
|
||||
if (modified.length > 0) {
|
||||
modified.forEach((r) => changes.push(
|
||||
`<div class="modified">modified <mark>${summariseRelay(r)}</mark></div>`,
|
||||
));
|
||||
}
|
||||
// list deletes
|
||||
const removed = next.filter((c) => !current.some((n) => n[1] === c[1]));
|
||||
if (removed.length > 0) {
|
||||
removed.forEach((r) => changes.push(
|
||||
`<div class="removed">removed <mark>${summariseRelay(r)}</mark></div>`,
|
||||
));
|
||||
}
|
||||
}
|
||||
return {
|
||||
ago: e.created_at,
|
||||
changes,
|
||||
option: i === 0
|
||||
? '<ins>Backup Complete<ins>'
|
||||
: `<a href="#" id="restore-contacts-${i}" class="secondary" onclick="event.preventDefault()">Restore</a>`,
|
||||
};
|
||||
});
|
||||
|
||||
export const generateHistoryTable = (history: Event[] | null):string => {
|
||||
if (!history || history.length === 0) return '<p>none</p>';
|
||||
let changes:VersionChange[];
|
||||
if (history[0].kind === 0) changes = generateMetadataChanges(history);
|
||||
else if (history[0].kind === 3) changes = generateContactsChanges(history as Kind3Event[]);
|
||||
else if (history[0].kind === 10002) changes = generateRelayChanges(history as Kind10002Event[]);
|
||||
else changes = [];
|
||||
return generateChangesTable(changes);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,136 @@
|
||||
import { fetchCachedProfileEvent, fetchCachedProfileEventHistory, publishEvent } from './fetchEvents';
|
||||
import { generateHistoryTable, Kind10002Event, Kind10002Tag } from './LoadHistory';
|
||||
import { localStorageGetItem } from './LocalStorage';
|
||||
|
||||
const generateRelayFormRow = (index:number, tag?:Kind10002Tag):string => `
|
||||
<tr id="PM-form-relay-${index}-row" class="relayformrow">
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="PM-form-relay-${index}-address"
|
||||
id="PM-form-relay-${index}-address"
|
||||
placeholder="Relay address: wss://..."
|
||||
${tag ? `value="${tag[1]}"` : ''}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<label for="PM-form-relay-${index}-read">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="PM-form-relay-${index}-read"
|
||||
name="PM-form-relay-${index}-read"
|
||||
${!tag || !tag[2] || tag[2] === 'read' ? 'checked="checked"' : ''}
|
||||
>
|
||||
read
|
||||
</label>
|
||||
<label for="PM-form-relay-${index}-write">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="PM-form-relay-${index}-write"
|
||||
name="PM-form-relay-${index}-write"
|
||||
${!tag || !tag[2] || tag[2] === 'write' ? 'checked="checked"' : ''}
|
||||
>
|
||||
write
|
||||
</label>
|
||||
</td>
|
||||
<td><a
|
||||
href="#"
|
||||
onclick="this.parentNode.parentNode.remove();return false;"
|
||||
class="button outline secondary"
|
||||
>Remove</a></td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
const generateRelayForm = (event: Kind10002Event | null):string => `
|
||||
<form id="relaysform">
|
||||
<table role="grid">
|
||||
<tbody id="relayformtbody">
|
||||
${event?.tags.map((a, i) => generateRelayFormRow(i, a)).join('')}
|
||||
${generateRelayFormRow(event ? event.tags.length : 0)}
|
||||
<tr id="relaybuttons">
|
||||
<td>
|
||||
<button id="relayssubmitbutton" type="button">${event ? 'Update' : 'Save'}</button>
|
||||
</td>
|
||||
<td>
|
||||
<button id="relaysresetbutton" class="secondary outline" type="reset">Reset Form</button>
|
||||
</td>
|
||||
<td>
|
||||
<button id="relaysaddbutton" class="secondary" type="button">Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const SubmitRelayForm = async () => {
|
||||
// set loading status
|
||||
const b = document.getElementById('relayssubmitbutton') as HTMLFormElement;
|
||||
b.setAttribute('disabled', '');
|
||||
b.setAttribute('aria-busy', 'true');
|
||||
b.innerHTML = 'Signing...';
|
||||
// construct and populate new content object with form data. avoid reordering properties
|
||||
const fd = new FormData(document.getElementById('relaysform') as HTMLFormElement);
|
||||
const tags = Array.from(Array(100)).map((_e, i) => {
|
||||
const url = fd.get(`PM-form-relay-${i}-address`);
|
||||
if (!url || url === '') return null;
|
||||
const w = !!fd.get(`PM-form-relay-${i}-write`);
|
||||
const r = !!fd.get(`PM-form-relay-${i}-read`);
|
||||
const base:Kind10002Tag = ['r', url as string];
|
||||
if (w && r) return base;
|
||||
return ['r', fd.get(`PM-form-relay-${i}-address`), r ? 'read' : 'write'] as Kind10002Tag;
|
||||
}).filter((v) => v !== null);
|
||||
// sign event
|
||||
if (!window.nostr) return;
|
||||
const ne = await window.nostr.signEvent({
|
||||
pubkey: localStorageGetItem('pubkey') as string,
|
||||
kind: 10002,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: '',
|
||||
tags: tags as Kind10002Tag[],
|
||||
});
|
||||
// publish
|
||||
b.innerHTML = 'Sending...';
|
||||
await publishEvent(ne);
|
||||
b.removeAttribute('aria-busy');
|
||||
b.innerHTML = 'Recieved by Relays!';
|
||||
setTimeout(() => {
|
||||
b.innerHTML = 'Update';
|
||||
b.removeAttribute('disabled');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const loadRelayForm = (RootElementID:string) => {
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `<div class="relayform">
|
||||
<h3>Relays</h3>
|
||||
${generateRelayForm(fetchCachedProfileEvent(10002) as Kind10002Event)}
|
||||
</div>`;
|
||||
// form submit event
|
||||
(document.getElementById('relayssubmitbutton') as HTMLButtonElement).onclick = (event) => {
|
||||
SubmitRelayForm();
|
||||
event.preventDefault();
|
||||
};
|
||||
// reset form
|
||||
(document.getElementById('relaysresetbutton') as HTMLButtonElement).onclick = (event) => {
|
||||
loadRelayForm(RootElementID);
|
||||
event.preventDefault();
|
||||
};
|
||||
// add button
|
||||
(document.getElementById('relaysaddbutton') as HTMLButtonElement).onclick = (event) => {
|
||||
const count = (document.getElementById('relayformtbody') as HTMLElement).childElementCount - 1;
|
||||
const tr = document.createElement('tr');
|
||||
(document.getElementById('relaybuttons') as HTMLElement).before(tr);
|
||||
tr.outerHTML = generateRelayFormRow(count);
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
const loadRelaysBackupHistory = (RootElementID:string) => {
|
||||
const table = generateHistoryTable(fetchCachedProfileEventHistory(10002));
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `<div class="relaysbackuphistory">
|
||||
<h3>Relays</h3>
|
||||
<p>TODO</p>
|
||||
<h3>Relays</h3>${table}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
@@ -10,9 +138,11 @@ const LoadRelaysPage = () => {
|
||||
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
|
||||
o.innerHTML = `
|
||||
<div id="relayspage" class="container">
|
||||
<div id="relayforcontainer"></div>
|
||||
<div id="relaysbackuphistory"></div>
|
||||
<div>
|
||||
`;
|
||||
loadRelayForm('relayforcontainer');
|
||||
loadRelaysBackupHistory('relaysbackuphistory');
|
||||
};
|
||||
|
||||
|
||||
@@ -66,3 +66,9 @@ img[src=""] {
|
||||
margin: 10px 10px 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#relayform {
|
||||
input[type="text"] {
|
||||
margin-top: 17px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user