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] backup your profile events to offline browser storage
|
||||||
- [x] review changes between backups
|
- [x] review changes between backups
|
||||||
- [x] `0`
|
- [x] `0`
|
||||||
- [ ] `10002` and `2`
|
- [x] `10002`
|
||||||
- [x] `3`
|
- [x] `3`
|
||||||
- [ ] selectively restore previous versions
|
- [ ] selectively restore previous versions
|
||||||
- [x] download profile backup history as JSON file
|
- [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
|
- [ ] Contacts recommendation based off social graph
|
||||||
- [ ] Suggest updates to contact relay based on Contact's kind `10002` and `2` events
|
- [ ] Suggest updates to contact relay based on Contact's kind `10002` and `2` events
|
||||||
|
|
||||||
- [ ] Relays
|
- [x] Relays
|
||||||
- [ ] editable table of read / write relays kind `10002` event
|
- [x] editable table of read / write relays kind `10002` event
|
||||||
- [ ] auto suggestion of `10002` event based on contact's relays if no event present
|
- [ ] auto suggestion of `10002` event based on contact's relays if no event present
|
||||||
- [ ] evaluation of `10002` based on contact's
|
- [ ] evaluation of `10002` based on contact's
|
||||||
- [ ] decentralisation score to encourage users not to use the same relay
|
- [ ] decentralisation score to encourage users not to use the same relay
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
generateContactsChanges, generateHistoryTable, generateMetadataChanges, Kind3Event,
|
generateContactsChanges, generateHistoryTable, generateMetadataChanges, generateRelayChanges,
|
||||||
|
Kind10002Event, Kind3Event,
|
||||||
} from './LoadHistory';
|
} from './LoadHistory';
|
||||||
import { MetadataFlex } from './LoadMetadataPage';
|
import { MetadataFlex } from './LoadMetadataPage';
|
||||||
import SampleEvents from './SampleEvents';
|
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 => {
|
export const generateHistoryTable = (history: Event[] | null):string => {
|
||||||
if (!history || history.length === 0) return '<p>none</p>';
|
if (!history || history.length === 0) return '<p>none</p>';
|
||||||
let changes:VersionChange[];
|
let changes:VersionChange[];
|
||||||
if (history[0].kind === 0) changes = generateMetadataChanges(history);
|
if (history[0].kind === 0) changes = generateMetadataChanges(history);
|
||||||
else if (history[0].kind === 3) changes = generateContactsChanges(history as Kind3Event[]);
|
else if (history[0].kind === 3) changes = generateContactsChanges(history as Kind3Event[]);
|
||||||
|
else if (history[0].kind === 10002) changes = generateRelayChanges(history as Kind10002Event[]);
|
||||||
else changes = [];
|
else changes = [];
|
||||||
return generateChangesTable(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 loadRelaysBackupHistory = (RootElementID:string) => {
|
||||||
|
const table = generateHistoryTable(fetchCachedProfileEventHistory(10002));
|
||||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||||
.innerHTML = `<div class="relaysbackuphistory">
|
.innerHTML = `<div class="relaysbackuphistory">
|
||||||
<h3>Relays</h3>
|
<h3>Relays</h3>${table}
|
||||||
<p>TODO</p>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,9 +138,11 @@ const LoadRelaysPage = () => {
|
|||||||
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
|
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
|
||||||
o.innerHTML = `
|
o.innerHTML = `
|
||||||
<div id="relayspage" class="container">
|
<div id="relayspage" class="container">
|
||||||
|
<div id="relayforcontainer"></div>
|
||||||
<div id="relaysbackuphistory"></div>
|
<div id="relaysbackuphistory"></div>
|
||||||
<div>
|
<div>
|
||||||
`;
|
`;
|
||||||
|
loadRelayForm('relayforcontainer');
|
||||||
loadRelaysBackupHistory('relaysbackuphistory');
|
loadRelaysBackupHistory('relaysbackuphistory');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -66,3 +66,9 @@ img[src=""] {
|
|||||||
margin: 10px 10px 10px 0;
|
margin: 10px 10px 10px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#relayform {
|
||||||
|
input[type="text"] {
|
||||||
|
margin-top: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user