diff --git a/readme.md b/readme.md
index 709785b..d3e5f8b 100644
--- a/readme.md
+++ b/readme.md
@@ -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
diff --git a/src/LoadHistory.test.ts b/src/LoadHistory.test.ts
index 3acb0d5..920c6b8 100644
--- a/src/LoadHistory.test.ts
+++ b/src/LoadHistory.test.ts
@@ -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(['
added wss://brando-relay1.com write only
']);
+ });
+ 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(['removed wss://brando-relay1.com write only
']);
+ });
+ 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(['modified wss://brando-relay.com read only
']);
+ });
+ 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([
+ 'added wss://brando-relay1.com write only
',
+ 'removed wss://alicerelay.example.com
',
+ ]);
+ });
+});
diff --git a/src/LoadHistory.ts b/src/LoadHistory.ts
index 75088ae..6c2aba3 100644
--- a/src/LoadHistory.ts
+++ b/src/LoadHistory.ts
@@ -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(
+ `added ${summariseRelay(r)}
`,
+ ));
+ }
+ // 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(
+ `modified ${summariseRelay(r)}
`,
+ ));
+ }
+ // list deletes
+ const removed = next.filter((c) => !current.some((n) => n[1] === c[1]));
+ if (removed.length > 0) {
+ removed.forEach((r) => changes.push(
+ `removed ${summariseRelay(r)}
`,
+ ));
+ }
+ }
+ return {
+ ago: e.created_at,
+ changes,
+ option: i === 0
+ ? 'Backup Complete'
+ : `Restore`,
+ };
+});
+
export const generateHistoryTable = (history: Event[] | null):string => {
if (!history || history.length === 0) return 'none
';
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);
};
diff --git a/src/LoadRelaysPage.ts b/src/LoadRelaysPage.ts
index 29e105d..2009da6 100644
--- a/src/LoadRelaysPage.ts
+++ b/src/LoadRelaysPage.ts
@@ -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 => `
+
+ |
+
+ |
+
+
+
+ |
+ Remove |
+
+`;
+
+const generateRelayForm = (event: Kind10002Event | null):string => `
+
+`;
+
+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 = `
+
Relays
+ ${generateRelayForm(fetchCachedProfileEvent(10002) as Kind10002Event)}
+ `;
+ // 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 = `
-
Relays
-
TODO
+
Relays
${table}
`;
};
@@ -10,9 +138,11 @@ const LoadRelaysPage = () => {
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
o.innerHTML = `
+
`;
+ loadRelayForm('relayforcontainer');
loadRelaysBackupHistory('relaysbackuphistory');
};
diff --git a/src/style.scss b/src/style.scss
index b6d9351..8884a05 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -66,3 +66,9 @@ img[src=""] {
margin: 10px 10px 10px 0;
}
}
+
+#relayform {
+ input[type="text"] {
+ margin-top: 17px;
+ }
+}
\ No newline at end of file