diff --git a/apps/ssb/tf-app.js b/apps/ssb/tf-app.js index 75d3fab8..e6e67e89 100644 --- a/apps/ssb/tf-app.js +++ b/apps/ssb/tf-app.js @@ -16,7 +16,9 @@ class TfElement extends LitElement { following: {type: Array}, users: {type: Object}, ids: {type: Array}, - tags: {type: Array}, + channels: {type: Array}, + channels_unread: {type: Object}, + channels_latest: {type: Object}, }; } @@ -33,7 +35,9 @@ class TfElement extends LitElement { this.following = []; this.users = {}; this.loaded = false; - this.tags = []; + this.channels = []; + this.channels_unread = {}; + this.channels_latest = {}; tfrpc.rpc.getBroadcasts().then((b) => { self.broadcasts = b || []; }); @@ -64,6 +68,27 @@ class TfElement extends LitElement { let ids = (await tfrpc.rpc.getIdentities()) || []; this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.ids = ids; + + let channels = await tfrpc.rpc.query(` + SELECT + content ->> 'channel' AS channel, + content ->> 'subscribed' AS subscribed + FROM + messages + WHERE + author = ? AND + content ->> 'type' = 'channel' + ORDER BY sequence + `, [this.whoami]); + let channel_map = {}; + for (let row of channels) { + if (row.subscribed) { + channel_map[row.channel] = true; + } else { + delete channel_map[row.channel]; + } + } + this.channels = Object.keys(channel_map); } set_hash(hash) { @@ -195,33 +220,9 @@ class TfElement extends LitElement { } } - async load_recent_tags() { - let start = new Date(); - this.tags = await tfrpc.rpc.query( - ` - WITH - recent AS (SELECT id, json(content) AS content FROM messages - WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post' - ORDER BY timestamp DESC LIMIT 1024), - recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag - FROM recent - WHERE json_extract(content, '$.channel') IS NOT NULL), - recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag - FROM recent, json_each(recent.content, '$.mentions') AS mention - WHERE json_valid(mention.value) AND tag LIKE '#%'), - combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions), - by_message AS (SELECT DISTINCT id, tag FROM combined) - SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10 - `, - [new Date() - 7 * 24 * 60 * 60 * 1000] - ); - console.log('tags took', (new Date() - start) / 1000.0, 'seconds'); - } - async load() { let whoami = this.whoami; - let tags = this.load_recent_tags(); - let following = await tfrpc.rpc.following([whoami], 3); + let following = await tfrpc.rpc.following([whoami], 2); let users = {}; let by_count = []; for (let [id, v] of Object.entries(following)) { @@ -233,6 +234,17 @@ class TfElement extends LitElement { }; by_count.push({count: v.of, id: id}); } + let channels = tfrpc.rpc.query(` + SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages + JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value + JOIN json_each(?2) AS following ON messages.author = following.value + WHERE messages.content ->> 'type' = 'post' AND messages.content ->> 'root' IS NULL + GROUP by channel + UNION + SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages + JOIN json_each(?2) AS following ON messages.author = following.value + `, [JSON.stringify(this.channels), JSON.stringify(Object.keys(following))]); + this.channels_unread = JSON.parse((await tfrpc.rpc.databaseGet('unread')) ?? '{}'); console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20)); let start_time = new Date(); users = await this.fetch_about(Object.keys(following).sort(), users); @@ -243,14 +255,24 @@ class TfElement extends LitElement { Object.keys(users).length, 'users' ); + channels = await channels; + this.channels_latest = Object.fromEntries(channels.map(x => [x.channel, x.rowid])); + console.log('CHANNELS', channels); this.following = Object.keys(following); this.users = users; - await tags; console.log(`load finished ${whoami} => ${this.whoami}`); this.whoami = whoami; this.loaded = whoami; } + channel_set_unread(event) { + console.log(event.detail.channel ?? '', event.detail.unread); + this.channels_unread[event.detail.channel ?? ''] = event.detail.unread; + this.channels_unread = Object.assign({}, this.channels_unread); + console.log(this.channels_unread); + tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread)); + } + render_tab() { let following = this.following; let users = this.users; @@ -265,6 +287,10 @@ class TfElement extends LitElement { .unread=${this.unread} @refresh=${() => (this.unread = [])} ?loading=${this.loading} + .channels=${this.channels} + .channels_latest=${this.channels_latest} + .channels_unread=${this.channels_unread} + @channelsetunread=${this.channel_set_unread} > `; } else if (this.tab === 'connections') { @@ -344,7 +370,7 @@ class TfElement extends LitElement { }; let tabs = html` -
+
`; } diff --git a/apps/ssb/tf-compose.js b/apps/ssb/tf-compose.js index 3ea22849..3b38467c 100644 --- a/apps/ssb/tf-compose.js +++ b/apps/ssb/tf-compose.js @@ -14,6 +14,7 @@ class TfComposeElement extends LitElement { apps: {type: Object}, drafts: {type: Object}, author: {type: String}, + channel: {type: String}, }; } @@ -196,6 +197,7 @@ class TfComposeElement extends LitElement { let message = { type: 'post', text: edit.innerText, + channel: this.channel, }; if (this.root || this.branch) { message.root = this.root; @@ -535,6 +537,9 @@ class TfComposeElement extends LitElement { class="w3-card-4 w3-theme-d4 w3-padding-small" style="box-sizing: border-box" > + ${this.channel !== undefined ? + html`

To #${this.channel}:

` : + undefined} ${this.render_encrypt()}
diff --git a/apps/ssb/tf-message.js b/apps/ssb/tf-message.js index ceb64d30..5701e75d 100644 --- a/apps/ssb/tf-message.js +++ b/apps/ssb/tf-message.js @@ -14,6 +14,8 @@ class TfMessageElement extends LitElement { format: {type: String}, blog_data: {type: String}, expanded: {type: Object}, + channel: {type: String}, + channel_unread: {type: Number}, }; } @@ -28,6 +30,7 @@ class TfMessageElement extends LitElement { this.drafts = {}; this.format = 'message'; this.expanded = {}; + this.channel_unread = -1; } show_reply() { @@ -312,12 +315,25 @@ ${JSON.stringify(mention, null, 2)}` )}`; } } } + mark_read() { + this.dispatchEvent(new CustomEvent('channelsetunread', { + bubbles: true, + composed: true, + detail: { + channel: this.channel, + unread: this.message.rowid + 1, + }, + })); + } + render_channels() { let content = this.message?.content; if (this?.messsage?.decrypted?.type == 'post') { @@ -344,7 +360,7 @@ ${JSON.stringify(mention, null, 2)}= this.channel_unread ? 'w3-theme-d2' : 'w3-theme-d4'); let self = this; let raw_button; switch (this.format) { @@ -423,6 +439,8 @@ ${JSON.stringify(mention, null, 2)} ` )} @@ -442,6 +460,8 @@ ${JSON.stringify(mention, null, 2)}` )}
`; @@ -463,6 +483,8 @@ ${JSON.stringify(mention, null, 2)} ` )} @@ -618,6 +640,11 @@ ${JSON.stringify(content, null, 2)} React + ${!content.root ? + html` + + ` : + undefined}

${this.render_children()}
diff --git a/apps/ssb/tf-news.js b/apps/ssb/tf-news.js index 816f10a6..6645e940 100644 --- a/apps/ssb/tf-news.js +++ b/apps/ssb/tf-news.js @@ -11,6 +11,8 @@ class TfNewsElement extends LitElement { following: {type: Array}, drafts: {type: Object}, expanded: {type: Object}, + channel: {type: String}, + channel_unread: {type: Number}, }; } @@ -25,6 +27,7 @@ class TfNewsElement extends LitElement { this.following = []; this.drafts = {}; this.expanded = {}; + this.channel_unread = -1; } process_messages(messages) { @@ -179,7 +182,7 @@ class TfNewsElement extends LitElement { this.finalize_messages(messages_by_id) ); return html` -
+
${final_messages.map( (x) => html`` )}
diff --git a/apps/ssb/tf-tab-news-feed.js b/apps/ssb/tf-tab-news-feed.js index f6c455d1..d93d1e06 100644 --- a/apps/ssb/tf-tab-news-feed.js +++ b/apps/ssb/tf-tab-news-feed.js @@ -12,6 +12,9 @@ class TfTabNewsFeedElement extends LitElement { messages: {type: Array}, drafts: {type: Object}, expanded: {type: Object}, + channels_unread: {type: Object}, + loading: {type: Number}, + time_range: {type: Array}, }; } @@ -26,19 +29,25 @@ class TfTabNewsFeedElement extends LitElement { this.following = []; this.drafts = {}; this.expanded = {}; - this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000; + this.channels_unread = {}; + this.start_time = (new Date()).valueOf(); + this.time_range = [0, 0]; } - async fetch_messages() { + channel() { + return this.hash.startsWith('##') ? this.hash.substring(2) : ''; + } + + async fetch_messages(start_time, end_time) { if (this.hash.startsWith('#@')) { let r = await tfrpc.rpc.query( ` - WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature + WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature FROM messages WHERE messages.author = ? ORDER BY sequence DESC LIMIT 20) - SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM mine JOIN messages_refs ON mine.id = messages_refs.ref JOIN messages ON messages_refs.message = messages.id @@ -62,24 +71,27 @@ class TfTabNewsFeedElement extends LitElement { `, [this.hash.substring(1)] ); - } else { + } else if (this.hash.startsWith('##')) { let promises = []; const k_following_limit = 256; for (let i = 0; i < this.following.length; i += k_following_limit) { promises.push( tfrpc.rpc.query( ` - WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM messages JOIN json_each(?) AS following ON messages.author = following.value - WHERE messages.timestamp > ? AND messages.timestamp < ? + WHERE + messages.timestamp > ? AND + messages.timestamp < ? AND + messages.content ->> 'channel' = ? ORDER BY messages.timestamp DESC) - SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.ref JOIN messages ON messages_refs.message = messages.id UNION - SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature FROM news JOIN messages_refs ON news.id = messages_refs.message JOIN messages ON messages_refs.ref = messages.id @@ -88,12 +100,42 @@ class TfTabNewsFeedElement extends LitElement { `, [ JSON.stringify(this.following.slice(i, i + k_following_limit)), - this.start_time, - /* - ** Don't show messages more than a day into the future to prevent - ** messages with far-future timestamps from staying at the top forever. - */ - new Date().valueOf() + 24 * 60 * 60 * 1000, + start_time, + end_time, + this.hash.substring(2), + ] + ) + ); + } + return [].concat(...(await Promise.all(promises))); + } else { + let promises = []; + const k_following_limit = 256; + for (let i = 0; i < this.following.length; i += k_following_limit) { + promises.push( + tfrpc.rpc.query( + ` + WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + FROM messages + JOIN json_each(?) AS following ON messages.author = following.value + WHERE messages.timestamp > ? AND messages.timestamp < ? + ORDER BY messages.timestamp DESC) + SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + FROM news + JOIN messages_refs ON news.id = messages_refs.ref + JOIN messages ON messages_refs.message = messages.id + UNION + SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature + FROM news + JOIN messages_refs ON news.id = messages_refs.message + JOIN messages ON messages_refs.ref = messages.id + UNION + SELECT news.* FROM news + `, + [ + JSON.stringify(this.following.slice(i, i + k_following_limit)), + start_time, + end_time, ] ) ); @@ -103,31 +145,19 @@ class TfTabNewsFeedElement extends LitElement { } async load_more() { - let last_start_time = this.start_time; - this.start_time = last_start_time - 24 * 60 * 60 * 1000; - let more = await tfrpc.rpc.query( - ` - WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature - FROM messages - JOIN json_each(?) AS following ON messages.author = following.value - WHERE messages.timestamp > ? - AND messages.timestamp <= ? - ORDER BY messages.timestamp DESC) - SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature - FROM news - JOIN messages_refs ON news.id = messages_refs.ref - JOIN messages ON messages_refs.message = messages.id - UNION - SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature - FROM news - JOIN messages_refs ON news.id = messages_refs.message - JOIN messages ON messages_refs.ref = messages.id - UNION - SELECT news.* FROM news - `, - [JSON.stringify(this.following), this.start_time, last_start_time] - ); - this.messages = await this.decrypt([...more, ...this.messages]); + this.loading++; + try { + let more = []; + while (!more.length) { + let last_start_time = this.start_time; + this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000; + more = await this.fetch_messages(this.start_time, last_start_time); + this.time_range = [this.start_time, this.time_range[1]]; + } + this.messages = await this.decrypt([...more, ...this.messages]); + } finally { + this.loading--; + } } async decrypt(messages) { @@ -160,6 +190,51 @@ class TfTabNewsFeedElement extends LitElement { this.messages = await this.decrypt([...messages, ...this.messages]); } + async load_messages() { + let self = this; + this.loading = true; + let messages = []; + try { + this.messages = []; + this._messages_hash = this.hash; + this._messages_following = this.following; + let now = new Date().valueOf(); + let start_time = now - 24 * 60 * 60 * 1000; + this.start_time = start_time; + this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000]; + messages = await this.fetch_messages(this.time_range[0], this.time_range[1]); + messages = await this.decrypt(messages); + if (!messages.length) { + let more = []; + while (!more.length && start_time >= 0) { + let last_start_time = start_time; + start_time = last_start_time - 7 * 24 * 60 * 60 * 1000; + more = await this.fetch_messages(start_time, last_start_time); + } + this.time_range = [start_time, this.time_range[1]]; + messages = await this.decrypt([...more, ...this.messages]); + } + } finally { + this.loading = false; + } + this.messages = messages; + console.log(`loading messages done for ${self.whoami}`); + } + + mark_all_read() { + let newest = this.messages.reduce((accumulator, current) => Math.max(accumulator, current.rowid), -1); + if (newest >= 0) { + this.dispatchEvent(new CustomEvent('channelsetunread', { + bubbles: true, + composed: true, + detail: { + channel: this.channel(), + unread: newest + 1, + }, + })); + } + } + render() { if ( !this.messages || @@ -169,27 +244,17 @@ class TfTabNewsFeedElement extends LitElement { console.log( `loading messages for ${this.whoami} (following ${this.following.length})` ); - let self = this; - this.messages = []; - this._messages_hash = this.hash; - this._messages_following = this.following; - this.fetch_messages() - .then(this.decrypt.bind(this)) - .then(function (messages) { - self.messages = messages; - console.log(`loading mesages done for ${self.whoami}`); - }) - .catch(function (error) { - alert(JSON.stringify(error, null, 2)); - }); + this.load_messages(); } let more; if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) { more = html`

- + + Showing ${new Date(this.time_range[0]).toLocaleDateString()} - ${new Date(this.time_range[1]).toLocaleDateString()}.

`; } @@ -202,6 +267,8 @@ class TfTabNewsFeedElement extends LitElement { .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded} + channel=${this.channel()} + channel_unread=${this.channels_unread?.[this.channel()]} > ${more} `; diff --git a/apps/ssb/tf-tab-news.js b/apps/ssb/tf-tab-news.js index 4b3fca2a..22b109d0 100644 --- a/apps/ssb/tf-tab-news.js +++ b/apps/ssb/tf-tab-news.js @@ -13,6 +13,9 @@ class TfTabNewsElement extends LitElement { drafts: {type: Object}, expanded: {type: Object}, loading: {type: Boolean}, + channels: {type: Array}, + channels_unread: {type: Object}, + channels_latest: {type: Object}, }; } @@ -29,6 +32,9 @@ class TfTabNewsElement extends LitElement { this.cache = {}; this.drafts = {}; this.expanded = {}; + this.channels_unread = {}; + this.channels_latest = {}; + this.channels = []; tfrpc.rpc.localStorageGet('drafts').then(function (d) { self.drafts = JSON.parse(d || '{}'); }); @@ -106,6 +112,47 @@ class TfTabNewsElement extends LitElement { } } + unread_status(channel) { + if (this.channels_latest[channel] && + (this.channels_unread[channel] === undefined || + this.channels_unread[channel] < this.channels_latest[channel])) { + return '🔵'; + } + } + + show_sidebar() { + this.renderRoot.getElementById('sidebar').style.display = 'block'; + this.renderRoot.getElementById('main').style.marginLeft = '2in'; + this.renderRoot.getElementById('show_sidebar').style.display = 'none'; + } + + hide_sidebar() { + this.renderRoot.getElementById('sidebar').style.display = 'none'; + this.renderRoot.getElementById('main').style.marginLeft = '0'; + this.renderRoot.getElementById('show_sidebar').style.display = 'block'; + } + + async channel_toggle_subscribed() { + let channel = this.hash.substring(2); + let subscribed = this.channels.indexOf(channel) != -1; + subscribed = !subscribed; + + await tfrpc.rpc.appendMessage(this.whoami, { + type: 'channel', + channel: channel, + subscribed: subscribed, + }); + if (subscribed) { + this.channels = [].concat([channel], this.channels).sort(); + } else { + this.channels = this.channels.filter(x => x != channel); + } + } + + channel() { + return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; + } + render() { let profile = this.hash.startsWith('#@') ? html``; } return html` -

- -

-
- Welcome, ! - ${edit_profile} + -
- + +

+ + ${this.hash.startsWith('##') ? + html` + + ` : + undefined} +

+
+ Welcome, ! + ${edit_profile} +
+
+ +
+ ${profile} +
+ @tf-expand=${this.on_expand} + .channels_unread=${this.channels_unread} + >
- ${profile} - `; } }