diff --git a/src/index.html b/src/index.html
index 42e5f25..ae2c206 100644
--- a/src/index.html
+++ b/src/index.html
@@ -102,7 +102,9 @@
diff --git a/src/main.ts b/src/main.ts
index eedcf7e..47b13ba 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,13 +4,13 @@ import {elem} from './utils/dom';
import {bounce} from './utils/time';
import {isWssUrl} from './utils/url';
import {closeSettingsView, config, toggleSettingsView} from './settings';
-import {sub24hFeed, subEventID, subNote, subProfile} from './subscriptions'
-import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt} from './events';
+import {subGlobalFeed, subEventID, subNote, subProfile, subPubkeys, subOwnContacts} from './subscriptions'
+import {getReplyTo, hasEventTag, isEvent, isMention, sortByCreatedAt, sortEventCreatedAt} from './events';
import {clearView, getViewContent, getViewElem, getViewOptions, setViewElem, view} from './view';
import {handleReaction, handleUpvote} from './reactions';
import {closePublishView, openWriteInput, togglePublishView} from './write';
import {handleMetadata, renderProfile} from './profiles';
-import {followContact, getContactUpdateMessage, setContactList, updateContactList} from './contacts';
+import {followContact, getContactUpdateMessage, getContacts, resetContactList, setContactList, updateContactList, updateFollowBtn} from './contacts';
import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes';
import {createTextNote, renderEventDetails, renderRecommendServer, renderUpdateContact} from './ui';
@@ -55,7 +55,6 @@ const renderFeed = bounce(() => {
.forEach(renderNote);
break;
case 'profile':
- const isEvent = (evt?: T): evt is T => evt !== undefined;
[
...textNoteList // get notes
.filter(note => note.pubkey === view.id),
@@ -69,6 +68,20 @@ const renderFeed = bounce(() => {
renderProfile(view.id);
break;
+ case 'home':
+ const ids = getContacts();
+ [
+ ...textNoteList
+ .filter(note => ids.includes(note.pubkey)),
+ ...replyList // search id in notes and replies
+ .filter(reply => ids.includes(reply.pubkey))
+ .map(reply => textNoteList.find(note => note.id === reply.replyTo))
+ .filter(isEvent),
+ ]
+ .sort(sortByCreatedAt)
+ .reverse()
+ .forEach(renderNote);
+ break;
case 'feed':
const now = Math.floor(Date.now() * 0.001);
textNoteList
@@ -87,7 +100,7 @@ const renderFeed = bounce(() => {
const renderReply = (evt: EventWithNip19AndReplyTo) => {
const parent = getViewElem(evt.replyTo);
- if (!parent) { // root article has not been rendered
+ if (!parent || getViewElem(evt.id)) {
return;
}
let replyContainer = parent.querySelector('.mbox-replies');
@@ -110,7 +123,6 @@ const handleReply = (evt: EventWithNip19, relay: string) => {
}
const replyTo = getReplyTo(evt);
if (!replyTo) {
- console.warn('expected to find reply-to-event-id', evt);
return;
}
const evtWithReplyTo = {replyTo, ...evt};
@@ -124,7 +136,7 @@ const handleTextNote = (evt: Event, relay: string) => {
return;
}
if (eventRelayMap[evt.id]) {
- eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: just push?
+ eventRelayMap[evt.id] = [...(eventRelayMap[evt.id]), relay]; // TODO: remove eventRelayMap and just check for getViewElem?
} else {
eventRelayMap[evt.id] = [relay];
const evtWithNip19 = {
@@ -145,12 +157,14 @@ const handleTextNote = (evt: Event, relay: string) => {
}
};
-config.rerenderFeed = () => {
+const rerenderFeed = () => {
clearView();
renderFeed();
};
+config.rerenderFeed = rerenderFeed;
const handleContactList = (evt: Event, relay: string) => {
+ // TODO: if newer and view.type === 'home' rerenderFeed()
setContactList(evt);
const view = getViewOptions();
if (
@@ -229,9 +243,21 @@ const onEvent = (evt: Event, relay: string) => {
// subscribe and change view
const route = (path: string) => {
+ const contactList = getContacts();
if (path === '/') {
- sub24hFeed(onEvent);
- view('/', {type: 'feed'});
+ if (contactList.length) {
+ const {pubkey} = config;
+ subPubkeys(contactList, onEvent);
+ view(`/`, {type: 'home'});
+ } else {
+ subGlobalFeed(onEvent);
+ view('/feed', {type: 'feed'});
+ }
+ return;
+ }
+ if (path === '/feed') {
+ subGlobalFeed(onEvent);
+ view('/feed', {type: 'feed'});
} else if (path.length === 64 && path.match(/^\/[0-9a-z]+$/)) {
const {type, data} = nip19.decode(path.slice(1));
if (typeof data !== 'string') {
@@ -246,6 +272,7 @@ const route = (path: string) => {
case 'npub':
subProfile(data, onEvent);
view(path, {type: 'profile', id: data});
+ updateFollowBtn(data);
break;
default:
console.warn(`type ${type} not yet supported`);
@@ -261,6 +288,7 @@ const route = (path: string) => {
};
// onload
+subOwnContacts(onEvent);
route(location.pathname);
// only push a new entry if there is no history onload
@@ -286,8 +314,10 @@ const handleLink = (a: HTMLAnchorElement, e: MouseEvent) => {
}
if (
href === '/'
+ || href.startsWith('/feed')
|| href.startsWith('/note')
|| href.startsWith('/npub')
+ || href.length === 65
) {
route(href);
history.pushState({}, '', href);
@@ -306,20 +336,26 @@ const handleButton = (button: HTMLButtonElement) => {
case 'back':
closePublishView();
return;
+ case 'import':
+ resetContactList(config.pubkey);
+ rerenderFeed();
+ subOwnContacts(onEvent);
+ subGlobalFeed(onEvent);
+ return;
}
const id = button.dataset.id || (button.closest('[data-id]') as HTMLElement)?.dataset.id;
if (id) {
switch(button.name) {
case 'reply':
openWriteInput(button, id);
- break;
+ return;
case 'star':
- const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id));
- note && handleUpvote(note);
- break;
+ const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id));
+ note && handleUpvote(note);
+ return;
case 'follow':
followContact(id);
- break;
+ return;
}
}
// const container = e.target.closest('[data-append]');
diff --git a/src/styles/view.css b/src/styles/view.css
index 9b1dda4..81260dd 100644
--- a/src/styles/view.css
+++ b/src/styles/view.css
@@ -24,14 +24,16 @@ aside {
}
nav {
+ align-items: center;
background-color: var(--bgcolor-nav);
display: flex;
flex-direction: row;
flex-grow: 1;
flex-shrink: 0;
justify-content: space-between;
+ min-height: 4.6rem;
overflow-y: auto;
- padding: 0 1.5rem;
+ padding: .2rem 1.5rem;
user-select: none;
-webkit-user-select: none;
}
@@ -42,6 +44,7 @@ nav {
}
@media (orientation: landscape) {
nav {
+ align-items: stretch;
flex-direction: column;
justify-content: space-between;
}
@@ -51,6 +54,8 @@ nav button {
--bgcolor-accent: transparent;
--border-color: transparent;
border-radius: 0;
+ color: inherit;
+ font-weight: bold;
padding: 1rem;
}
@media (orientation: landscape) {
@@ -58,6 +63,17 @@ nav button {
nav button {
padding: 2rem 0;
}
+ nav .spacer {
+ flex-grow: 1;
+ }
+ nav button:last-child {
+ margin-bottom: .4rem;
+ }
+}
+@media (orientation: portrait) {
+ nav .spacer {
+ display: none;
+ }
}
.view {
@@ -121,8 +137,6 @@ nav .content {
justify-content: space-between;
}
nav a {
- display: flex;
- flex-direction: column;
text-align: center;
text-decoration: none;
}
diff --git a/src/subscriptions.ts b/src/subscriptions.ts
index 82979ef..8a70215 100644
--- a/src/subscriptions.ts
+++ b/src/subscriptions.ts
@@ -8,8 +8,58 @@ type SubCallback = (
relay: string,
) => void;
+export const subPubkeys = (
+ pubkeys: string[],
+ onEvent: SubCallback,
+) => {
+ const authorsPrefixes = pubkeys.map(pubkey => pubkey.slice(0, 32));
+ console.info(`subscribe to homefeed ${authorsPrefixes}`);
+ unsubAll();
+
+ const repliesTo = new Set();
+ sub({
+ cb: (evt, relay) => {
+ if (
+ evt.tags.some(hasEventTag)
+ && !evt.tags.some(isMention)
+ ) {
+ const note = getReplyTo(evt); // get all reply to events instead?
+ if (note && !repliesTo.has(note)) {
+ repliesTo.add(note);
+ subOnce({
+ cb: onEvent,
+ filter: {
+ ids: [note],
+ kinds: [1],
+ limit: 1,
+ },
+ relay,
+ });
+ }
+ }
+ onEvent(evt, relay);
+ },
+ filter: {
+ authors: authorsPrefixes,
+ kinds: [1],
+ limit: 20,
+ },
+ });
+ // get metadata
+ sub({
+ cb: onEvent,
+ filter: {
+ authors: pubkeys,
+ kinds: [0],
+ limit: pubkeys.length,
+ },
+ unsub: true,
+ });
+};
+
/** subscribe to global feed */
-export const sub24hFeed = (onEvent: SubCallback) => {
+export const subGlobalFeed = (onEvent: SubCallback) => {
+ console.info('subscribe to global feed');
unsubAll();
const now = Math.floor(Date.now() * 0.001);
const pubkeys = new Set();
@@ -132,15 +182,17 @@ export const subNote = (
});
};
- replies.add(eventId)
- sub({
- cb: onReply,
- filter: {
- '#e': [eventId],
- kinds: [1, 7],
- },
- unsub: true,
- });
+ replies.add(eventId);
+ setTimeout(() => {
+ sub({
+ cb: onReply,
+ filter: {
+ '#e': [eventId],
+ kinds: [1, 7],
+ },
+ unsub: true, // TODO: probably keep this subscription also after onReply/unsubAll
+ });
+ }, 200);
};
/** subscribe to npub key (nip-19) */
@@ -206,11 +258,33 @@ export const subEventID = (
id: string,
onEvent: SubCallback,
) => {
+ unsubAll();
sub({
cb: onEvent,
filter: {
ids: [id],
limit: 1,
},
+ unsub: true,
+ });
+ sub({
+ cb: onEvent,
+ filter: {
+ authors: [id],
+ limit: 200,
+ },
+ unsub: true,
+ });
+};
+
+export const subOwnContacts = (onEvent: SubCallback) => {
+ sub({
+ cb: onEvent,
+ filter: {
+ authors: [config.pubkey],
+ kinds: [3],
+ limit: 1,
+ },
+ unsub: true,
});
};
diff --git a/src/template.ts b/src/template.ts
index 20cb28b..60bc842 100644
--- a/src/template.ts
+++ b/src/template.ts
@@ -7,7 +7,9 @@ export type DOMMap = {
};
export type ViewTemplateOptions = {
- type: 'feed'
+ type: 'home';
+} | {
+ type: 'feed';
} | {
type: 'note';
id: string;
@@ -23,8 +25,13 @@ export const renderViewTemplate = (options: ViewTemplateOptions) => {
const content = elem('div', {className: 'content'});
const dom: DOMMap = {};
switch (options.type) {
+ case 'home':
+ break;
+ case 'feed':
+ break;
case 'profile':
const pubkey = options.id;
+ const npub = nip19.npubEncode(pubkey);
const detail = elem('p');
const followStatus = elem('small');
const followBtn = elem('button', {
@@ -34,7 +41,7 @@ export const renderViewTemplate = (options: ViewTemplateOptions) => {
}, 'follow');
const following = elem('span');
const profileHeader = elem('header', {className: 'hero'}, [
- elem('small', {className: 'hero-npub'}, nip19.npubEncode(pubkey)),
+ elem('small', {className: 'hero-npub'}, npub),
elem('div', {className: 'hero-title'}, [
elem('h1', {}, pubkey),
followStatus,
@@ -53,8 +60,6 @@ export const renderViewTemplate = (options: ViewTemplateOptions) => {
break;
case 'note':
break;
- case 'feed':
- break;
case 'event':
const id = options.id;
content.append(
diff --git a/src/view.ts b/src/view.ts
index 6992406..82e1d3e 100644
--- a/src/view.ts
+++ b/src/view.ts
@@ -60,8 +60,8 @@ type GetViewOptions = () => ViewTemplateOptions;
/**
* get options for current view
- * @returns {id: 'feed' | 'profile' | 'note' | 'event', id?: string}
- */
+ * @returns {id: 'home' | 'feed' | 'profile' | 'note' | 'event', id?: string}
+*/
export const getViewOptions: GetViewOptions = () => containers[activeContainerIndex]?.options || {type: 'feed'};
/**