mirror of
https://github.com/aljazceru/nostr-profile-manager.git
synced 2025-12-17 05:34:20 +01:00
added contacts features
This commit is contained in:
24
readme.md
24
readme.md
@@ -1,6 +1,6 @@
|
||||
# Nostr Profile Manager
|
||||
|
||||
Lightweight and efficent typescript micro app for basic nostr profile management. Current USP is offline backup and restore.
|
||||
Lightweight typescript micro app for basic nostr profile management. Current USP is offline backup and restore.
|
||||
|
||||
Only javascript dependancy is [nostr-tools](https://github.com/nbd-wtf/nostr-tools). no JS frameworks. no state management tools.
|
||||
|
||||
@@ -27,10 +27,12 @@ Supported profile events: kind `0`, `10002` and `3`.
|
||||
- [x] profile and banner previews
|
||||
- [x] preserve, edit and remove custom properties
|
||||
|
||||
- [ ] Contacts
|
||||
- [ ] Add Contacts based on nip05, npub or hex
|
||||
- [ ] Remove Contacts
|
||||
- [ ] Edit petname and relay
|
||||
- [x] Contacts
|
||||
- [x] Add Contacts based on nip05, nip19 (npub, nprofile or naddr) or hex
|
||||
- [x] keyword search profiles to find contacts
|
||||
- [ ] keyword search profiles to find contacts of contacts
|
||||
- [x] Remove Contacts
|
||||
- [x] Edit petname and relay
|
||||
- [ ] Suggestions Engine
|
||||
- [ ] Contacts recommendation based off social graph
|
||||
- [ ] Suggest updates to contact relay based on Contact's kind `10002` and `2` events
|
||||
@@ -41,9 +43,17 @@ Supported profile events: kind `0`, `10002` and `3`.
|
||||
- [ ] evaluation of `10002` based on contact's
|
||||
- [ ] decentralisation score to encourage users not to use the same relay
|
||||
|
||||
##### Lightweight and Efficent
|
||||
- [ ] manage event distribution to relays
|
||||
- [ ] Show which and how many relays return an each event (and including historic events)
|
||||
- [ ] Show warning if selected write relays don't
|
||||
- [ ] suggest republishing events (particularly `10002`) to spread them to more relays if appropriate
|
||||
|
||||
- [ ] look far and wide for events
|
||||
- cycle through all known relays to find current and previous versions of profile events to enable restoration. reccommended only when accessed through a VPN
|
||||
##### Lightweight
|
||||
- [ ] only javascript dependancy is nostr-tools (TODO: remove timeago)
|
||||
- [x] connects to the minimum number of relays
|
||||
- [x] connect relays specified in `10002` or 3 default relays
|
||||
- [x] minimises the number of open websockets
|
||||
- [ ] minimises the number of open websockets
|
||||
- [x] use blastr relay to send profile events far and wide
|
||||
- [ ] efficent (TODO: currently the 'Contacts' functionality is very inefficent)
|
||||
|
||||
@@ -1,13 +1,400 @@
|
||||
import { loadBackupHistory } from './LoadHistory';
|
||||
import { Event, nip05, nip19 } from 'nostr-tools';
|
||||
import {
|
||||
fetchAllCachedProfileEvents, fetchCachedMyProfileEvent, fetchCachedProfileEvent,
|
||||
fetchProfileEvent, getContactMostPopularPetname, getContactName, getMyPetnameForUser,
|
||||
getMyRelayForUser, isUserMyContact, submitUnsignedEvent,
|
||||
} from './fetchEvents';
|
||||
import { Kind3Event, loadBackupHistory } from './LoadHistory';
|
||||
import { localStorageGetItem } from './LocalStorage';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* > 'Suggested Housekeeping' section that:
|
||||
* > highligts out suggestions (deleted, do not use, compromised, old profile)
|
||||
* > add relays
|
||||
*/
|
||||
const getPubkey = (e:Event | string) => (typeof e === 'string' ? e : e.pubkey);
|
||||
|
||||
const generateMicroCardLi = (eventorpubkey:Event | string):string => {
|
||||
const pubkey = getPubkey(eventorpubkey);
|
||||
return `
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
onclick="return false;"
|
||||
class="microcard ${typeof eventorpubkey === 'string' ? 'nokind0' : ''}"
|
||||
id="contact-microcard-${pubkey}"
|
||||
>
|
||||
${getContactName(pubkey)}
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
};
|
||||
|
||||
const generateMicroCardList = (eventorpubkeys:(Event | string)[]):string => `
|
||||
<ul id="maincontactlist">${eventorpubkeys.map(
|
||||
(e) => generateMicroCardLi(typeof e === 'string' ? e : e.pubkey),
|
||||
).join('')}</ul>
|
||||
`;
|
||||
|
||||
const generateContactDetails = (pubkey:string):string => {
|
||||
const e = fetchCachedProfileEvent(pubkey, 0);
|
||||
if (!e) {
|
||||
return `
|
||||
<article>
|
||||
<strong>${getContactName(pubkey)}</strong>
|
||||
<p>loading users metadata...</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
const m = JSON.parse(e.content);
|
||||
const otherspetname = getContactMostPopularPetname(pubkey);
|
||||
const ismycontact = isUserMyContact(pubkey);
|
||||
return `
|
||||
<article>
|
||||
<div>
|
||||
${m && !!m.picture ? `<img src="${m.picture}" /> ` : ''}
|
||||
<div class="contactdetailsmain">
|
||||
<strong>${m.name ? m.name : '[unknown name]'}</strong>
|
||||
${m.nip05 ? `<small id="nip05-${pubkey}">${m.nip05} </small>` : ''}<span id="nip05-${pubkey}-verified"></span>
|
||||
${otherspetname && otherspetname !== m.name ? `<div>popular petname: ${otherspetname}</div>` : ''}
|
||||
<div><small>${m.about ? m.about : ''}</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="contactdetailsform">
|
||||
<div class="grid">
|
||||
<label for="mypetname">
|
||||
petname
|
||||
<input
|
||||
type="text"
|
||||
name="mypetname"
|
||||
placeholder="petname"
|
||||
value="${getMyPetnameForUser(pubkey) || ''}"
|
||||
id="petname-contact-form-${pubkey}"
|
||||
onkeypress = "this.onchange();"
|
||||
onpaste = "this.onchange();"
|
||||
oninput = "this.onchange();"
|
||||
>
|
||||
</label>
|
||||
<label for="myrelay">
|
||||
relay
|
||||
<input
|
||||
type="text"
|
||||
name="myrelay"
|
||||
placeholder="relay"
|
||||
value="${getMyRelayForUser(pubkey) || ''}"
|
||||
id="relay-contact-form-${pubkey}"
|
||||
onkeypress = "this.onchange();"
|
||||
onpaste = "this.onchange();"
|
||||
oninput = "this.onchange();"
|
||||
>
|
||||
</label>
|
||||
<div class="contact-form-buttons">
|
||||
<button id="add-contact-${pubkey}" class="${ismycontact ? 'hide' : ''}">Add</button>
|
||||
<button id="update-contact-${pubkey}" class="hide">Update</button>
|
||||
<button id="remove-contact-${pubkey}" class="${ismycontact ? '' : 'hide'}">Remove Contact</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
`;
|
||||
};
|
||||
|
||||
const loadContactDetails = (pubkey:string):void => {
|
||||
// load html
|
||||
(document.getElementById('contactdetails') as HTMLDivElement)
|
||||
.innerHTML = generateContactDetails(pubkey);
|
||||
// scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
// reload if no kind0
|
||||
const reload = () => setTimeout(() => loadContactDetails(pubkey), 500);
|
||||
if (!fetchCachedProfileEvent(pubkey, 0)) reload();
|
||||
// on form change show update button instead of remove button
|
||||
const onchange = () => {
|
||||
if (!isUserMyContact(pubkey)) return;
|
||||
(document.getElementById(`update-contact-${pubkey}`) as HTMLButtonElement)
|
||||
.classList.remove('hide');
|
||||
(document.getElementById(`remove-contact-${pubkey}`) as HTMLButtonElement)
|
||||
.classList.add('hide');
|
||||
};
|
||||
(document.getElementById(`relay-contact-form-${pubkey}`) as HTMLInputElement)
|
||||
.onchange = onchange;
|
||||
(document.getElementById(`petname-contact-form-${pubkey}`) as HTMLInputElement)
|
||||
.onchange = onchange;
|
||||
// nip05
|
||||
const checkUserNip05 = async () => {
|
||||
const nip05el = document.getElementById(`nip05-${pubkey}`);
|
||||
if (nip05el) {
|
||||
const addr = nip05el.innerHTML.trim();
|
||||
let verified:boolean = false;
|
||||
try {
|
||||
const r = await nip05.queryProfile(addr);
|
||||
verified = !!r && r.pubkey === pubkey;
|
||||
} catch { /* empty */ }
|
||||
const verifiedel = (document.getElementById(`nip05-${pubkey}-verified`) as HTMLElement);
|
||||
if (verified) verifiedel.innerHTML = '<ins>✔ verified</ins>';
|
||||
else verifiedel.innerHTML = '<del>✔ verified</del>';
|
||||
}
|
||||
};
|
||||
checkUserNip05();
|
||||
// add / update / remove buttons
|
||||
const generateTag = (): ['p', string, string, string] => [
|
||||
'p',
|
||||
pubkey,
|
||||
(document.getElementById(`relay-contact-form-${pubkey}`) as HTMLInputElement).value || '',
|
||||
(document.getElementById(`petname-contact-form-${pubkey}`) as HTMLInputElement).value || '',
|
||||
];
|
||||
const addUpdateOrRemoveContact = async (tags:['p', string, string, string][], ButtonID: string) => {
|
||||
await submitUnsignedEvent(
|
||||
{
|
||||
pubkey: localStorageGetItem('pubkey') as string,
|
||||
kind: 3,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: '',
|
||||
tags,
|
||||
},
|
||||
ButtonID,
|
||||
);
|
||||
loadContactDetails(pubkey);
|
||||
};
|
||||
// add button
|
||||
(document.getElementById(`add-contact-${pubkey}`) as HTMLButtonElement).onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const ce = fetchCachedMyProfileEvent(3) as Kind3Event || null;
|
||||
const tags = ce ? [...ce.tags, generateTag()] : [generateTag()];
|
||||
addUpdateOrRemoveContact(tags, `add-contact-${pubkey}`);
|
||||
};
|
||||
// update button
|
||||
(document.getElementById(`update-contact-${pubkey}`) as HTMLButtonElement).onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const ce = fetchCachedMyProfileEvent(3) as Kind3Event;
|
||||
const tags = [...ce.tags.map((t) => (t[1] === pubkey ? generateTag() : t))];
|
||||
addUpdateOrRemoveContact(tags, `update-contact-${pubkey}`);
|
||||
};
|
||||
// remove button
|
||||
(document.getElementById(`remove-contact-${pubkey}`) as HTMLButtonElement).onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const ce = fetchCachedMyProfileEvent(3) as Kind3Event;
|
||||
const tags = [...ce.tags.filter((t) => (t[1] !== pubkey))];
|
||||
addUpdateOrRemoveContact(tags, `remove-contact-${pubkey}`);
|
||||
};
|
||||
};
|
||||
|
||||
const processHexOrNip19 = (s:string):{ pubkey: string | null; relays: string[] | null } => {
|
||||
let pubkey:string | null = null;
|
||||
let relays:string[] | null = null;
|
||||
// is hex string?
|
||||
const regexhex64 = /^[a-fA-F0-9]{64}$/i;
|
||||
if (regexhex64.test(s)) {
|
||||
pubkey = s; // this could be an event id?
|
||||
return { pubkey, relays };
|
||||
}
|
||||
// check for nip19
|
||||
try {
|
||||
const { data, type } = nip19.decode(s) as {
|
||||
type: string, data: {
|
||||
pubkey?:string, // not present in nevent, nsec or note
|
||||
relays?: string[];
|
||||
}
|
||||
};
|
||||
if (typeof data === 'string') {
|
||||
if (type === 'npub') pubkey = data;
|
||||
else throw new Error('no pubkey');
|
||||
} else {
|
||||
if (data.pubkey) pubkey = data.pubkey;
|
||||
if (data.relays) relays = data.relays;
|
||||
}
|
||||
} catch { /* empty */ }
|
||||
return { pubkey, relays };
|
||||
};
|
||||
|
||||
const nip05cache:{ [nip05Search: string]: string | null } = {};
|
||||
|
||||
let nip05Searching = '';
|
||||
const searchNip05 = async (input: HTMLInputElement):Promise<null | 'searching' | string> => {
|
||||
const s = input.value;
|
||||
const searchstatus = document.getElementById('searchstatus') as HTMLDivElement;
|
||||
const setLoading = () => { searchstatus.innerHTML = '<p aria-busy="true">Searching nip05...<p>'; };
|
||||
// check valid NIP05 string
|
||||
if (!!s || (s.indexOf('.') === -1 && s.indexOf('@') === -1)) return null;
|
||||
// check cache
|
||||
if (nip05cache[s]) return nip05cache[s];
|
||||
if (nip05cache[s] === null) return null;
|
||||
// check if already searching
|
||||
if (s === nip05Searching) return 'searching';
|
||||
// wait for typing to pause
|
||||
const repsonse = await new Promise((finished) => {
|
||||
setTimeout(async () => {
|
||||
// if value hasn't changed
|
||||
if (input.value === s) {
|
||||
// set loading
|
||||
setLoading();
|
||||
// prevent duplicate queries
|
||||
nip05Searching = s;
|
||||
// make request call
|
||||
try {
|
||||
const r = await nip05.queryProfile(s);
|
||||
if (!!r && r.pubkey) nip05cache[s] = r.pubkey;
|
||||
else nip05cache[s] = null;
|
||||
} catch {
|
||||
// mark as not valid
|
||||
nip05cache[s] = null;
|
||||
}
|
||||
// if search query has changed return 'searching' to end
|
||||
if (s === nip05Searching) finished('searching');
|
||||
else {
|
||||
// reset nip05Searching;
|
||||
nip05Searching = '';
|
||||
// return value
|
||||
finished(nip05cache[s]);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}) as null | string;
|
||||
return repsonse;
|
||||
};
|
||||
|
||||
const generateResults = (eventorpubkeys:(Event | string)[]) => {
|
||||
const mycontacts = eventorpubkeys;
|
||||
return `
|
||||
<div id="contactdetails"></div>
|
||||
<div id="mycontacts">
|
||||
<h6>My Contacts</h6>
|
||||
${generateMicroCardList(mycontacts)}
|
||||
</div>
|
||||
<div id="mycontactscontacts"></div>
|
||||
<div id="otherusers"></div>
|
||||
`;
|
||||
};
|
||||
|
||||
const refreshResults = (eventorpubkeys?:(Event | string)[]) => {
|
||||
(document.getElementById('searchstatus') as HTMLDivElement).innerHTML = '';
|
||||
// if called without array load my contacts
|
||||
if (!eventorpubkeys) {
|
||||
const e3 = fetchCachedMyProfileEvent(3);
|
||||
if (e3) refreshResults(e3.tags.map((c) => c[1]));
|
||||
return;
|
||||
}
|
||||
// display results
|
||||
(document.getElementById('searchresults') as HTMLDivElement)
|
||||
.innerHTML = eventorpubkeys.length === 0
|
||||
? 'no results'
|
||||
: generateResults(eventorpubkeys);
|
||||
eventorpubkeys.forEach((e) => {
|
||||
const microcarda = document.getElementById(`contact-microcard-${getPubkey(e)}`) as HTMLAnchorElement;
|
||||
// activate buttons to view details.
|
||||
microcarda.onclick = () => {
|
||||
loadContactDetails(getPubkey(e));
|
||||
return false;
|
||||
};
|
||||
// replace pubkey with metadata when loaded
|
||||
const recheckForKind0 = () => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const ne = fetchCachedProfileEvent(getPubkey(e), 0);
|
||||
// this will add with kind 0 name or if its not found petnames from other users
|
||||
const name = getContactName(getPubkey(ne || e));
|
||||
if (name !== microcarda.innerHTML) microcarda.innerHTML = name;
|
||||
if (!ne) recheckForKind0();
|
||||
} catch { /* empty - assume element has been removed */ }
|
||||
}, 750);
|
||||
};
|
||||
if (microcarda.classList.contains('nokind0')) recheckForKind0();
|
||||
});
|
||||
// display details if only result
|
||||
if (eventorpubkeys.length === 1) loadContactDetails(getPubkey(eventorpubkeys[0]));
|
||||
};
|
||||
|
||||
const setSearchInputOnChangeEvent = () => {
|
||||
const input = document.getElementById('searchinput') as HTMLInputElement;
|
||||
const searchstatus = document.getElementById('searchstatus') as HTMLDivElement;
|
||||
input.onchange = async () => {
|
||||
const s = input.value;
|
||||
let pubkey: string | null = null;
|
||||
let relays: string[] | null = null;
|
||||
// no search
|
||||
if (!s || s.trim().length === 0) {
|
||||
const e3 = fetchCachedMyProfileEvent(3);
|
||||
if (e3) refreshResults(e3.tags.map((c) => c[1]));
|
||||
return;
|
||||
}
|
||||
// if 61+ assume hex, or nip19 (npub, nprofile, naddr) and get pubkey / relays
|
||||
if (s.length > 60) {
|
||||
({ pubkey, relays } = processHexOrNip19(s));
|
||||
if (!pubkey) {
|
||||
searchstatus.innerHTML = 'invalid search input - try npub, nprofile, naddr or hex';
|
||||
return;
|
||||
}
|
||||
searchstatus.innerHTML = 'extracted pubkey. searching for profile...';
|
||||
} else {
|
||||
// keyword search kind 0s
|
||||
const allkind0s = fetchAllCachedProfileEvents(0);
|
||||
const words = s.split(' ');
|
||||
const matches = allkind0s
|
||||
.filter((e) => words.map((w) => e.content.indexOf(w)).some((v) => v > -1));
|
||||
refreshResults(matches);
|
||||
// search nip05
|
||||
if (!!s && (s.indexOf('.') > -1 && s.indexOf('@') > -1)) {
|
||||
const r = await searchNip05(input);
|
||||
if (r === 'searching') return;
|
||||
if (!r) searchstatus.innerHTML = '<p>not a verified nip05 address</p>';
|
||||
else {
|
||||
searchstatus.innerHTML = '<p>Verified nip05 address. loading profile...</p>';
|
||||
pubkey = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if extracted pubkey - hex, nip19 or nip05
|
||||
if (pubkey) {
|
||||
const ce = fetchCachedProfileEvent(pubkey, 0);
|
||||
// kind 0 not found for user
|
||||
if (!ce) {
|
||||
// request from relay
|
||||
const r = await fetchProfileEvent(pubkey, 0, relays);
|
||||
// found not profile
|
||||
if (!r) {
|
||||
searchstatus.innerHTML = 'extracted pubkey. but couldn\'t find profile.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
refreshResults();
|
||||
loadContactDetails(pubkey);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const loadViewContacts = (RootElementID:string) => {
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `
|
||||
<div id="contactsearch">
|
||||
<input
|
||||
type="search"
|
||||
id="searchinput"
|
||||
placeholder="nip05, npub, nprofile or keywords for contacts of contacts"
|
||||
onkeypress = "this.onchange();"
|
||||
onpaste = "this.onchange();"
|
||||
oninput = "this.onchange();"
|
||||
>
|
||||
<div id="searchstatus"></div>
|
||||
</div>
|
||||
<div id="searchresults"></div>
|
||||
`;
|
||||
setSearchInputOnChangeEvent();
|
||||
refreshResults();
|
||||
};
|
||||
|
||||
const LoadContactsPage = () => {
|
||||
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
|
||||
o.innerHTML = `
|
||||
<div id="contactspage" class="container">
|
||||
<div id="viewcontacts"></div>
|
||||
<div id="contactsbackuphistory"></div>
|
||||
<div>
|
||||
`;
|
||||
loadBackupHistory('contactsbackuphistory', 3);
|
||||
loadViewContacts('viewcontacts');
|
||||
};
|
||||
|
||||
export default LoadContactsPage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as timeago from 'timeago.js';
|
||||
import { Event } from 'nostr-tools';
|
||||
import { fetchCachedProfileEventHistory, submitUnsignedEvent } from './fetchEvents';
|
||||
import { fetchCachedMyProfileEventHistory, submitUnsignedEvent } from './fetchEvents';
|
||||
|
||||
export type VersionChange = {
|
||||
ago:number;
|
||||
@@ -69,13 +69,7 @@ const sameContact = (
|
||||
|
||||
const getPetname = (a:['p', string, string, string]):string => {
|
||||
if (a[3] && a[3].length > 0) return `<mark>${a[3]}</mark>`;
|
||||
return `<mark>${(a[1]).substring(0, 10)}...</mark>`;
|
||||
/**
|
||||
* todo: add npubEncode
|
||||
* npubEncode is imported from nostr-tools and causes the jest test runner to fail with:
|
||||
* SyntaxError: Cannot use import statement outside a module
|
||||
*/
|
||||
// return `<mark>${npubEncode(a[1]).substring(0, 10)}...</mark>`;
|
||||
return `<mark id="history-petname-${a[1]}">${(a[1]).substring(0, 10)}...</mark>`;
|
||||
};
|
||||
|
||||
export const generateContactsChanges = (
|
||||
@@ -209,7 +203,7 @@ export const activateRestoreButtons = (history: Event[] | null, afterRestore: ()
|
||||
};
|
||||
|
||||
export const loadBackupHistory = (RootElementID:string, kind: 0 | 10002 | 3) => {
|
||||
const h = fetchCachedProfileEventHistory(kind);
|
||||
const h = fetchCachedMyProfileEventHistory(kind);
|
||||
const table = generateHistoryTable(h);
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `<h4>Backup History</h4>${table}`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { nip05 } from 'nostr-tools';
|
||||
import { fetchCachedProfileEvent, submitUnsignedEvent } from './fetchEvents';
|
||||
import { fetchCachedMyProfileEvent, submitUnsignedEvent } from './fetchEvents';
|
||||
import { loadBackupHistory } from './LoadHistory';
|
||||
import { localStorageGetItem } from './LocalStorage';
|
||||
|
||||
@@ -73,7 +73,7 @@ const SubmitMetadataForm = async () => {
|
||||
// construct and populate new content object with form data. avoid reordering properties
|
||||
const fd = new FormData(document.getElementById('metadataform') as HTMLFormElement);
|
||||
const n:{ [x: string]: unknown; } = {};
|
||||
const e = fetchCachedProfileEvent(0);
|
||||
const e = fetchCachedMyProfileEvent(0);
|
||||
(e ? [...(Object.keys(JSON.parse(e.content))), ...standardkeys] : standardkeys)
|
||||
.forEach((k) => {
|
||||
const d = fd.get(`PM-form-${k}`);
|
||||
@@ -95,7 +95,7 @@ const SubmitMetadataForm = async () => {
|
||||
};
|
||||
|
||||
const loadMetadataForm = (RootElementID:string) => {
|
||||
const e = fetchCachedProfileEvent(0);
|
||||
const e = fetchCachedMyProfileEvent(0);
|
||||
const MetadataContent = !e ? null : JSON.parse(e.content) as MetadataFlex;
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `<div class="profileform">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Event } from 'nostr-tools';
|
||||
import {
|
||||
fetchCachedProfileEvent, fetchCachedProfileEventHistory, hadLatest, isUptodate,
|
||||
fetchCachedMyProfileEvent, fetchCachedMyProfileEventHistory, hadLatest, isUptodate,
|
||||
} from './fetchEvents';
|
||||
import LoadContactsPage from './LoadContactsPage';
|
||||
import { LoadMetadataPage, MetadataFlex } from './LoadMetadataPage';
|
||||
@@ -126,7 +126,7 @@ export const generateBackupHeroHeading = (
|
||||
};
|
||||
|
||||
export const LoadProfileHome = () => {
|
||||
const noprofileinfo = !fetchCachedProfileEvent(0) && !fetchCachedProfileEvent(3);
|
||||
const noprofileinfo = !fetchCachedMyProfileEvent(0) && !fetchCachedMyProfileEvent(3);
|
||||
const uptodate = isUptodate();
|
||||
const hadlatest = hadLatest();
|
||||
const o:HTMLElement = document.getElementById('PM-container') as HTMLElement;
|
||||
@@ -134,11 +134,11 @@ export const LoadProfileHome = () => {
|
||||
<div class="container">
|
||||
<div class="hero grid">
|
||||
${noprofileinfo ? generateLogoHero() : `<div><article class="profile-summary-card">
|
||||
${generateMetadataHeader(fetchCachedProfileEvent(0) as Event)}
|
||||
${generateMetadataHeader(fetchCachedMyProfileEvent(0) as Event)}
|
||||
<div>
|
||||
${generateMetadataSummary(fetchCachedProfileEvent(0), !uptodate)}
|
||||
${generateContactsSummary(fetchCachedProfileEvent(3), !uptodate)}
|
||||
${generateRelaysSummary(fetchCachedProfileEvent(10002), !uptodate)}
|
||||
${generateMetadataSummary(fetchCachedMyProfileEvent(0), !uptodate)}
|
||||
${generateContactsSummary(fetchCachedMyProfileEvent(3), !uptodate)}
|
||||
${generateRelaysSummary(fetchCachedMyProfileEvent(10002), !uptodate)}
|
||||
</div>
|
||||
</article></div>`}
|
||||
<div>${generateBackupHeroHeading(uptodate, noprofileinfo, hadlatest)}</div>
|
||||
@@ -158,10 +158,10 @@ export const LoadProfileHome = () => {
|
||||
donwloada.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
const jsonStr = JSON.stringify([
|
||||
...(fetchCachedProfileEventHistory(0) || []),
|
||||
...(fetchCachedProfileEventHistory(2) || []),
|
||||
...(fetchCachedProfileEventHistory(10002) || []),
|
||||
...(fetchCachedProfileEventHistory(3) || []),
|
||||
...(fetchCachedMyProfileEventHistory(0) || []),
|
||||
...(fetchCachedMyProfileEventHistory(2) || []),
|
||||
...(fetchCachedMyProfileEventHistory(10002) || []),
|
||||
...(fetchCachedMyProfileEventHistory(3) || []),
|
||||
]);
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(jsonStr)}`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchCachedProfileEvent, submitUnsignedEvent } from './fetchEvents';
|
||||
import { fetchCachedMyProfileEvent, submitUnsignedEvent } from './fetchEvents';
|
||||
import { Kind10002Event, Kind10002Tag, loadBackupHistory } from './LoadHistory';
|
||||
import { localStorageGetItem } from './LocalStorage';
|
||||
|
||||
@@ -94,7 +94,7 @@ const loadRelayForm = (RootElementID:string) => {
|
||||
(document.getElementById(RootElementID) as HTMLDivElement)
|
||||
.innerHTML = `<div class="relayform">
|
||||
<h3>Relays</h3>
|
||||
${generateRelayForm(fetchCachedProfileEvent(10002) as Kind10002Event)}
|
||||
${generateRelayForm(fetchCachedMyProfileEvent(10002) as Kind10002Event)}
|
||||
</div>`;
|
||||
// form submit event
|
||||
(document.getElementById('relayssubmitbutton') as HTMLButtonElement).onclick = (event) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Event } from 'nostr-tools';
|
||||
import SampleEvents from './SampleEvents';
|
||||
import {
|
||||
storeMyProfileEvent, fetchCachedProfileEventHistory, fetchCachedProfileEvent,
|
||||
storeMyProfileEvent, fetchCachedMyProfileEventHistory, fetchCachedMyProfileEvent,
|
||||
fetchMyProfileEvents,
|
||||
} from './fetchEvents';
|
||||
import * as LocalStorage from './LocalStorage';
|
||||
@@ -43,7 +43,7 @@ describe('', () => {
|
||||
describe('stores event, returns true and calls updateLastUpdateDate() when', () => {
|
||||
test('first event submitted', () => {
|
||||
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`] });
|
||||
expect(fetchCachedProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
|
||||
expect(fetchCachedMyProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
|
||||
expect(r).toBeTruthy();
|
||||
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -56,7 +56,7 @@ describe('', () => {
|
||||
storeMyProfileEvent(a[0]);
|
||||
const r2 = storeMyProfileEvent(a[1]);
|
||||
const r3 = storeMyProfileEvent(a[2]);
|
||||
const r = fetchCachedProfileEventHistory(kind);
|
||||
const r = fetchCachedMyProfileEventHistory(kind);
|
||||
expect(r).toContainEqual(a[0]);
|
||||
expect(r).toContainEqual(a[1]);
|
||||
expect(r).toContainEqual(a[2]);
|
||||
@@ -69,14 +69,14 @@ describe('', () => {
|
||||
test('event from a different pubkey submitted', () => {
|
||||
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`], pubkey: '1' });
|
||||
expect(r).toBeFalsy();
|
||||
expect(fetchCachedProfileEventHistory(kind)).toBeNull();
|
||||
expect(fetchCachedMyProfileEventHistory(kind)).toBeNull();
|
||||
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
test('duplicate events (events with the same id)', () => {
|
||||
storeMyProfileEvent({ ...SampleEvents[`kind${kind}`] });
|
||||
const r = storeMyProfileEvent({ ...SampleEvents[`kind${kind}`], content: 'different' });
|
||||
expect(r).toBeFalsy();
|
||||
expect(fetchCachedProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
|
||||
expect(fetchCachedMyProfileEventHistory(kind)).toEqual([{ ...SampleEvents[`kind${kind}`] }]);
|
||||
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -86,11 +86,11 @@ describe('', () => {
|
||||
expect(updateLastUpdatedSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
describe('fetchCachedProfileEventHistory', () => {
|
||||
describe('fetchCachedMyProfileEventHistory', () => {
|
||||
describe('returns array of events', () => {
|
||||
test('single event', () => {
|
||||
storeMyProfileEvent({ ...SampleEvents.kind0 });
|
||||
expect(fetchCachedProfileEventHistory(0)).toEqual([{ ...SampleEvents.kind0 }]);
|
||||
expect(fetchCachedMyProfileEventHistory(0)).toEqual([{ ...SampleEvents.kind0 }]);
|
||||
});
|
||||
test('multiple events', () => {
|
||||
const a = [
|
||||
@@ -101,7 +101,7 @@ describe('', () => {
|
||||
storeMyProfileEvent(a[0]);
|
||||
storeMyProfileEvent(a[1]);
|
||||
storeMyProfileEvent(a[2]);
|
||||
const r = fetchCachedProfileEventHistory(0);
|
||||
const r = fetchCachedMyProfileEventHistory(0);
|
||||
expect(r).toContainEqual(a[0]);
|
||||
expect(r).toContainEqual(a[1]);
|
||||
expect(r).toContainEqual(a[2]);
|
||||
@@ -115,10 +115,10 @@ describe('', () => {
|
||||
storeMyProfileEvent(a[0]);
|
||||
storeMyProfileEvent(a[1]);
|
||||
storeMyProfileEvent(a[2]);
|
||||
const r = fetchCachedProfileEventHistory(0);
|
||||
const r = fetchCachedMyProfileEventHistory(0);
|
||||
expect(r).toContainEqual(a[0]);
|
||||
expect(r).toContainEqual(a[1]);
|
||||
expect(fetchCachedProfileEventHistory(3)).toContainEqual(a[2]);
|
||||
expect(fetchCachedMyProfileEventHistory(3)).toContainEqual(a[2]);
|
||||
});
|
||||
test('ordered by created_at decending', () => {
|
||||
const a = [
|
||||
@@ -129,7 +129,7 @@ describe('', () => {
|
||||
storeMyProfileEvent({ ...a[0] });
|
||||
storeMyProfileEvent({ ...a[1] });
|
||||
storeMyProfileEvent({ ...a[2] });
|
||||
const r = fetchCachedProfileEventHistory(0) as Event[];
|
||||
const r = fetchCachedMyProfileEventHistory(0) as Event[];
|
||||
expect(r[0]).toEqual(a[1]);
|
||||
expect(r[1]).toEqual(a[2]);
|
||||
expect(r[2]).toEqual(a[0]);
|
||||
@@ -137,10 +137,10 @@ describe('', () => {
|
||||
});
|
||||
test('returns null if no events of kind present', () => {
|
||||
storeMyProfileEvent({ ...SampleEvents.kind0 });
|
||||
expect(fetchCachedProfileEventHistory(3)).toBeNull();
|
||||
expect(fetchCachedMyProfileEventHistory(3)).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('fetchCachedProfileEvent', () => {
|
||||
describe('fetchCachedMyProfileEvent', () => {
|
||||
test('returns event of specified kind with largest created_at value', () => {
|
||||
const a = [
|
||||
{ ...SampleEvents.kind0 },
|
||||
@@ -150,23 +150,23 @@ describe('', () => {
|
||||
storeMyProfileEvent({ ...a[0] });
|
||||
storeMyProfileEvent({ ...a[1] });
|
||||
storeMyProfileEvent({ ...a[2] });
|
||||
const r = fetchCachedProfileEvent(0) as Event;
|
||||
const r = fetchCachedMyProfileEvent(0) as Event;
|
||||
expect(r).toEqual(a[1]);
|
||||
});
|
||||
test('returns null if no events of kind present', () => {
|
||||
storeMyProfileEvent({ ...SampleEvents.kind0 });
|
||||
expect(fetchCachedProfileEventHistory(3)).toBeNull();
|
||||
expect(fetchCachedMyProfileEventHistory(3)).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('fetchMyProfileEvents', () => {
|
||||
const fetchCachedProfileEventSpy = jest.spyOn(FetchEvents, 'fetchCachedProfileEvent');
|
||||
const fetchCachedMyProfileEventSpy = jest.spyOn(FetchEvents, 'fetchCachedMyProfileEvent');
|
||||
const mockEventProcessor = jest.fn();
|
||||
const mockrequestEventsFromRelays = jest.spyOn(RelayManagement, 'requestEventsFromRelays');
|
||||
const mockupdateLastFetchDate = jest.spyOn(FetchEvents, 'updateLastFetchDate');
|
||||
const mocklastFetchDate = jest.spyOn(FetchEvents, 'lastFetchDate');
|
||||
const mockisUptodate = jest.spyOn(FetchEvents, 'isUptodate');
|
||||
beforeEach(async () => {
|
||||
fetchCachedProfileEventSpy.mockReset();
|
||||
fetchCachedMyProfileEventSpy.mockReset();
|
||||
mockEventProcessor.mockReset();
|
||||
mockrequestEventsFromRelays.mockReset();
|
||||
mockupdateLastFetchDate.mockReset();
|
||||
@@ -176,7 +176,7 @@ describe('', () => {
|
||||
describe('when isUptodate returns true', () => {
|
||||
beforeEach(async () => {
|
||||
mockisUptodate.mockReturnValue(true);
|
||||
fetchCachedProfileEventSpy.mockImplementation((kind) => {
|
||||
fetchCachedMyProfileEventSpy.mockImplementation((kind) => {
|
||||
if (kind === 0) return { ...SampleEvents.kind0 };
|
||||
if (kind === 10002) return null;
|
||||
if (kind === 2) return null;
|
||||
@@ -192,15 +192,15 @@ describe('', () => {
|
||||
expect(mockupdateLastFetchDate).toBeCalledTimes(0);
|
||||
});
|
||||
test('eventProcessor called with latest event from cache for each profile kind with event(s)', async () => {
|
||||
expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(0);
|
||||
expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(3);
|
||||
expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(0);
|
||||
expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(3);
|
||||
expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind0 });
|
||||
expect(mockEventProcessor).toBeCalledWith({ ...SampleEvents.kind3 });
|
||||
expect(mockEventProcessor).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
test('eventProcessor not called on profile event kind that isn\'t present', async () => {
|
||||
expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(10002);
|
||||
expect(fetchCachedProfileEventSpy).toHaveBeenCalledWith(2);
|
||||
expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(10002);
|
||||
expect(fetchCachedMyProfileEventSpy).toHaveBeenCalledWith(2);
|
||||
expect(mockEventProcessor).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -215,7 +215,7 @@ describe('', () => {
|
||||
});
|
||||
await fetchMyProfileEvents(SampleEvents.kind0.pubkey, mockEventProcessor);
|
||||
};
|
||||
test('1 write relays, function called with custom relay and 2 default relays', async () => {
|
||||
test('1 relay, function called with custom relay and 2 default relays', async () => {
|
||||
storeMyProfileEvent({
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
@@ -234,7 +234,7 @@ describe('', () => {
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
test('2 write relays function called with custom relays and 1 default relays', async () => {
|
||||
test('2 relays function called with custom relays and 1 default relays', async () => {
|
||||
storeMyProfileEvent({
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
@@ -254,7 +254,7 @@ describe('', () => {
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
test('2 write relays including first defauly relay. function called with custom relays and 1 different default relays', async () => {
|
||||
test('2 relays including first default relay. function called with custom relays and 1 different default relays', async () => {
|
||||
storeMyProfileEvent({
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
@@ -274,13 +274,13 @@ describe('', () => {
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
test('with 4 write relays function called with all custom relays', async () => {
|
||||
test('with 4 relays function called with all custom relays', async () => {
|
||||
storeMyProfileEvent({
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com'],
|
||||
['r', 'wss://expensive-relay.example2.com', 'write'],
|
||||
['r', 'wss://expensive-relay.example2.com', 'read'],
|
||||
['r', 'wss://alicerelay.example3.com'],
|
||||
],
|
||||
});
|
||||
@@ -297,28 +297,6 @@ describe('', () => {
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
test('custom read relays ignored', async () => {
|
||||
storeMyProfileEvent({
|
||||
...SampleEvents.kind10002,
|
||||
tags: [
|
||||
['r', 'wss://alicerelay.example.com'],
|
||||
['r', 'wss://brando-relay.com'],
|
||||
['r', 'wss://expensive-relay.example2.com', 'write'],
|
||||
['r', 'wss://nostr-relay.example.com', 'read'],
|
||||
],
|
||||
});
|
||||
await doBefore();
|
||||
expect(mockrequestEventsFromRelays).toBeCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
[
|
||||
'wss://alicerelay.example.com',
|
||||
'wss://brando-relay.com',
|
||||
'wss://expensive-relay.example2.com',
|
||||
],
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('and when no cached 10002 events are present and none are return', () => {
|
||||
const mockstoreMyProfileEvent = jest.spyOn(FetchEvents, 'storeMyProfileEvent');
|
||||
|
||||
@@ -71,7 +71,7 @@ export const storeMyProfileEvent = (event:Event): boolean => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const fetchCachedProfileEventHistory = (
|
||||
export const fetchCachedMyProfileEventHistory = (
|
||||
kind: 0 | 2 | 10002 | 3,
|
||||
): null | [Event, ...Event[]] => {
|
||||
// get data from local storage
|
||||
@@ -85,19 +85,19 @@ export const fetchCachedProfileEventHistory = (
|
||||
return a.sort((x, y) => y.created_at - x.created_at);
|
||||
};
|
||||
|
||||
export const fetchCachedProfileEvent = (kind: 0 | 2 | 10002 | 3): null | Event => {
|
||||
const a = fetchCachedProfileEventHistory(kind);
|
||||
export const fetchCachedMyProfileEvent = (kind: 0 | 2 | 10002 | 3): null | Event => {
|
||||
const a = fetchCachedMyProfileEventHistory(kind);
|
||||
if (a === null) return null;
|
||||
// return Event in array with most recent created_at date
|
||||
return a[0];
|
||||
};
|
||||
|
||||
const getRelays = () => {
|
||||
const e = fetchCachedProfileEvent(10002);
|
||||
const mywriterelays = !e ? [] : e.tags.filter((r) => !r[2] || r[2] === 'write').map((r) => r[1]);
|
||||
const e = fetchCachedMyProfileEvent(10002);
|
||||
const myrelays = !e ? [] : e.tags.map((r) => r[1]);
|
||||
// return minimum of 3 relays, filling in with default relays (removing duplicates)
|
||||
return mywriterelays.length > 3 ? mywriterelays : [...new Set([
|
||||
...mywriterelays,
|
||||
return myrelays.length > 3 ? myrelays : [...new Set([
|
||||
...myrelays,
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
'wss://nostr-relay.wlvs.space',
|
||||
@@ -118,7 +118,7 @@ export const fetchMyProfileEvents = async (
|
||||
}, starterrelays, [0, 2, 10002, 3]);
|
||||
// if new 10002 event found with more write relays
|
||||
if (
|
||||
fetchCachedProfileEvent(10002)?.tags
|
||||
fetchCachedMyProfileEvent(10002)?.tags
|
||||
.some((t) => starterrelays.indexOf(t[1]) === -1 && (!t[2] || t[2] === 'write'))
|
||||
) {
|
||||
// fetch events again to ensure we got all my profile events
|
||||
@@ -129,12 +129,169 @@ export const fetchMyProfileEvents = async (
|
||||
} else {
|
||||
// for kinds 0, 2, 10002 and 3
|
||||
[0, 2, 10002, 3].forEach((k) => {
|
||||
const e = fetchCachedProfileEvent(k as 0 | 2 | 10002 | 3);
|
||||
const e = fetchCachedMyProfileEvent(k as 0 | 2 | 10002 | 3);
|
||||
if (e !== null) profileEventProcesser(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const UserProfileEvents:{
|
||||
[pubkey: string]: {
|
||||
[kind: number]: Event;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const storeProfileEvent = (event:Event) => {
|
||||
if (!UserProfileEvents[event.pubkey]) UserProfileEvents[event.pubkey] = {};
|
||||
if (
|
||||
// no event of kind for pubkey
|
||||
!UserProfileEvents[event.pubkey][event.kind]
|
||||
// newer event of kind recieved
|
||||
|| UserProfileEvents[event.pubkey][event.kind].created_at < event.created_at
|
||||
) {
|
||||
// store it
|
||||
UserProfileEvents[event.pubkey][event.kind] = event;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMyContactsProfileEvents = async () => {
|
||||
const c = fetchCachedMyProfileEvent(3);
|
||||
if (!c || c.tags.length === 0) return;
|
||||
const required = c.tags.filter((p) => !UserProfileEvents[p[1]]);
|
||||
if (required.length > 0) {
|
||||
await requestEventsFromRelays(
|
||||
required.map((t) => t[1]),
|
||||
storeProfileEvent,
|
||||
getRelays(),
|
||||
[0, 10002, 3],
|
||||
);
|
||||
// TODO: check 10002 events and ensure we have read for one of their write relays
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchCachedProfileEvent = (pubkey:string, kind:0 | 10002 | 3):Event | null => {
|
||||
if (localStorageGetItem('pubkey') === pubkey) return fetchCachedMyProfileEvent(kind);
|
||||
if (!UserProfileEvents[pubkey]) return null;
|
||||
if (!UserProfileEvents[pubkey][kind]) return null;
|
||||
return UserProfileEvents[pubkey][kind];
|
||||
};
|
||||
|
||||
export const fetchAllCachedProfileEvents = (
|
||||
kind: 0 | 10002 | 3,
|
||||
):Event[] => Object.keys(UserProfileEvents)
|
||||
.filter((p) => !!UserProfileEvents[p][kind])
|
||||
.map((p) => UserProfileEvents[p][kind]);
|
||||
|
||||
export const fetchProfileEvents = async (
|
||||
pubkeys:[string, ...string[]],
|
||||
kind:0 | 10002 | 3,
|
||||
relays?: string[] | null,
|
||||
):Promise<[(Event | null), ...(Event | null)[]]> => {
|
||||
const notcached = pubkeys.filter((p) => !fetchCachedProfileEvent(p, kind));
|
||||
if (notcached.length > 0) {
|
||||
await requestEventsFromRelays(
|
||||
notcached,
|
||||
storeProfileEvent,
|
||||
relays || getRelays(),
|
||||
[0, 10002, 3],
|
||||
);
|
||||
}
|
||||
return pubkeys.map(
|
||||
(p) => fetchCachedProfileEvent(p, kind),
|
||||
) as [(Event | null), ...(Event | null)[]];
|
||||
};
|
||||
|
||||
export const fetchProfileEvent = async (
|
||||
pubkey:string,
|
||||
kind:0 | 10002 | 3,
|
||||
relays?: string[] | null,
|
||||
):Promise<Event | null> => {
|
||||
const r = await fetchProfileEvents([pubkey], kind, relays);
|
||||
return r[0];
|
||||
};
|
||||
|
||||
export const getContactMostPopularPetname = (pubkey: string):string | null => {
|
||||
// considered implementing frank.david.erin model in nip-02 but I think the UX is to confusing
|
||||
// get count of petnames for users by other contacts
|
||||
const petnamecounts: { [petname: string]: number } = Object.keys(UserProfileEvents)
|
||||
// returns petname or null
|
||||
.map((pk) => {
|
||||
if (!UserProfileEvents[pk][3]) return null;
|
||||
const petnametag = UserProfileEvents[pk][3].tags.find((t) => t[1] === pubkey && t[3]);
|
||||
if (petnametag) return petnametag[3];
|
||||
return null;
|
||||
})
|
||||
// returns petname counts
|
||||
.reduce((pv, c) => {
|
||||
if (!c) return pv;
|
||||
if (!pv[c]) return { ...pv, [c]: 1 };
|
||||
return { ...pv, [c]: pv[c] + 1 };
|
||||
}, {} as { [petname: string]: number });
|
||||
if (petnamecounts.length === 0) return null;
|
||||
// returns most frequent petname for user amoung contacts (appended with ' (?)')
|
||||
return Object.keys(petnamecounts).sort((a, b) => petnamecounts[b] - petnamecounts[a])[0];
|
||||
};
|
||||
|
||||
export const getMyPetnameForUser = (pubkey: string): string | null => {
|
||||
const e = fetchCachedMyProfileEvent(3);
|
||||
if (e) {
|
||||
const mypetname = e.tags.find((t) => t[1] === pubkey && t[3]);
|
||||
if (mypetname) return mypetname[3];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getMyRelayForUser = (pubkey: string): string | null => {
|
||||
const e = fetchCachedMyProfileEvent(3);
|
||||
if (e) {
|
||||
const relay = e.tags.find((t) => t[1] === pubkey && t[2] && t[2] !== '');
|
||||
if (relay) return relay[2];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const isUserMyContact = (pubkey: string): boolean | null => {
|
||||
const e = fetchCachedMyProfileEvent(3);
|
||||
if (e) {
|
||||
if (e.tags.some((t) => t[1] === pubkey)) return true;
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getContactName = (pubkey: string):string => {
|
||||
// my own name
|
||||
if (localStorageGetItem('pubkey') === pubkey) {
|
||||
const m = fetchCachedMyProfileEvent(0);
|
||||
if (m) {
|
||||
const { name } = JSON.parse(m.content);
|
||||
if (name) return name;
|
||||
}
|
||||
} else {
|
||||
// my petname for contact
|
||||
const mypetname = getMyPetnameForUser(pubkey);
|
||||
if (mypetname) return mypetname;
|
||||
// TODO: what about displaying a common petname in brackets if vastly different from their name?
|
||||
// their kind 0 name
|
||||
if (UserProfileEvents[pubkey]) {
|
||||
if (UserProfileEvents[pubkey][0]) {
|
||||
const { name } = JSON.parse(UserProfileEvents[pubkey][0].content);
|
||||
if (name) return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
// most popular petname for user amoung contacts
|
||||
const popularpetname = getContactMostPopularPetname(pubkey);
|
||||
if (popularpetname) return `${popularpetname} (?)`;
|
||||
// return shortened pubkey
|
||||
/**
|
||||
* TODO: add npubEncode
|
||||
* npubEncode is imported from nostr-tools and causes the jest test runner to fail with:
|
||||
* SyntaxError: Cannot use import statement outside a module
|
||||
*/
|
||||
return `${pubkey.substring(0, 10)}...`;
|
||||
};
|
||||
|
||||
export const publishEvent = async (event:Event):Promise<boolean> => {
|
||||
const r = await publishEventToRelay(
|
||||
event,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Event, UnsignedEvent } from 'nostr-tools';
|
||||
import { generateLogoHero, LoadProfileHome } from './LoadProfileHome';
|
||||
import { fetchMyProfileEvents } from './fetchEvents';
|
||||
import { fetchMyContactsProfileEvents, fetchMyProfileEvents } from './fetchEvents';
|
||||
import { localStorageGetItem, localStorageSetItem } from './LocalStorage';
|
||||
import { LoadMetadataPage } from './LoadMetadataPage';
|
||||
import LoadContactsPage from './LoadContactsPage';
|
||||
@@ -31,6 +31,8 @@ const loadProfile = async () => {
|
||||
(document.getElementById('navmetadata') as HTMLElement).onclick = LoadMetadataPage;
|
||||
(document.getElementById('navcontacts') as HTMLElement).onclick = LoadContactsPage;
|
||||
(document.getElementById('navrelays') as HTMLElement).onclick = LoadRelaysPage;
|
||||
// get events from my contacts
|
||||
await fetchMyContactsProfileEvents();
|
||||
};
|
||||
|
||||
const LoadLandingPage = () => {
|
||||
|
||||
@@ -71,4 +71,41 @@ img[src=""] {
|
||||
input[type="text"] {
|
||||
margin-top: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.hide { display:none;}
|
||||
|
||||
#contactdetails {
|
||||
img {
|
||||
display:inline-block;
|
||||
max-width: 20%;
|
||||
padding-right: 16px;
|
||||
vertical-align:top;
|
||||
}
|
||||
.contactdetailsmain {
|
||||
display:inline-block;
|
||||
width:75%;
|
||||
}
|
||||
.contact-form-buttons {
|
||||
margin-top: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
#searchinput { margin-top: 16px; }
|
||||
|
||||
#searchresults ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li {
|
||||
list-style: none;
|
||||
margin:0;
|
||||
width:30%;
|
||||
display: inline-block;
|
||||
}
|
||||
img {
|
||||
max-width: 75px;
|
||||
border-radius: 38px;
|
||||
float: left;
|
||||
margin: 10px 10px 10px 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user