Files
.gitea
apps
admin
api
apps
blog
db
follow
identity
intro
issues
journal
room
sneaker
ssb
app.js
commonmark-hashtag.js
commonmark.min.js
emojis.js
emojis.json
filesaver.min.js
filesaver.min.js.map
index.html
lit-all.min.js
lit-all.min.js.map
script.js
tf-app.js
tf-compose.js
tf-message.js
tf-news.js
tf-profile.js
tf-reactions-modal.js
tf-styles.js
tf-tab-connections.js
tf-tab-news-feed.js
tf-tab-news.js
tf-tab-query.js
tf-tab-search.js
tf-tag.js
tf-user.js
tf-utils.js
tribute.css
tribute.esm.js
storage
test
todo
web
welcome
wiki
admin.json
api.json
apps.json
blog.json
db.json
follow.json
identity.json
intro.json
issues.json
journal.json
room.json
sneaker.json
ssb.json
storage.json
test.json
todo.json
web.json
welcome.json
wiki.json
core
deps
docs
metadata
src
tools
.clang-format
.dockerignore
.git-blame-ignore-revs
.gitignore
.gitmodules
.prettierignore
.prettierrc.yaml
CONTRIBUTING.md
Dockerfile
Doxyfile
GNUmakefile
LICENSE
README.md
default.nix
flake.lock
flake.nix
package-lock.json
package.json
tildefriends/apps/ssb/tf-tab-news-feed.js

465 lines
14 KiB
JavaScript
Raw Normal View History

import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabNewsFeedElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
hash: {type: String},
following: {type: Array},
messages: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
channels_unread: {type: Object},
channels_latest: {type: Object},
loading: {type: Number},
time_range: {type: Array},
time_loading: {type: Array},
private_messages: {type: Array},
recent_reactions: {type: Array},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.hash = '#';
this.following = [];
this.drafts = {};
this.expanded = {};
this.channels_unread = {};
this.channels_latest = {};
2024-12-05 20:47:02 -05:00
this.start_time = new Date().valueOf();
this.time_range = [0, 0];
this.time_loading = undefined;
this.recent_reactions = [];
2024-12-04 20:28:57 -05:00
this.loading = 0;
}
channel() {
2024-12-05 20:47:02 -05:00
return this.hash.startsWith('##')
? this.hash.substring(2)
: this.hash.substring(1);
}
2025-04-09 18:50:14 -04:00
async _fetch_related_messages(messages) {
let refs = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(messages.map((x) => x.id))]
);
let related_messages = await tfrpc.rpc.query(
`
SELECT FALSE AS is_primary, 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(?2) refs ON messages.id = refs.value
JOIN json_each(?1) AS following ON messages.author = following.value
`,
[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
);
let combined = [].concat(messages, related_messages);
let refs2 = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(combined.map((x) => x.id))]
);
2025-04-09 22:47:23 -04:00
let t0 = new Date();
let result = [].concat(
2025-04-09 22:15:51 -04:00
combined,
await tfrpc.rpc.query(
`
2025-04-09 18:50:14 -04:00
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
2025-04-09 22:47:23 -04:00
FROM json_each(?2) refs
JOIN messages ON messages.id = refs.value
JOIN json_each(?1) following ON messages.author = following.value
WHERE messages.content ->> 'type' != 'post'
2025-04-09 18:50:14 -04:00
`,
2025-04-09 22:15:51 -04:00
[
JSON.stringify(this.following),
JSON.stringify(refs2.map((x) => x.ref)),
]
)
);
2025-04-09 22:47:23 -04:00
let t1 = new Date();
console.log((t1 - t0) / 1000);
return result;
2025-04-09 18:50:14 -04:00
}
async fetch_messages(start_time, end_time) {
this.time_loading = [start_time, end_time];
let result;
if (this.hash == '#@') {
result = await tfrpc.rpc.query(
`
2024-12-23 13:32:36 -05:00
WITH mentions 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_fts(?1)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.author != ?1 AND
2025-01-22 18:28:55 -05:00
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
2024-12-23 13:32:36 -05:00
ORDER BY timestamp DESC limit 20)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
2024-12-23 13:32:36 -05:00
FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT TRUE AS is_primary, * FROM mentions
2024-12-05 20:47:02 -05:00
`,
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
start_time,
end_time,
2024-12-05 20:47:02 -05:00
]
);
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
WITH
2025-01-22 18:28:55 -05:00
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
2025-01-22 18:28:55 -05:00
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence DESC LIMIT 20
)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected
JOIN messages_refs ON selected.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT TRUE AS is_primary, * FROM selected
`,
[this.hash.substring(1), start_time, end_time]
2024-02-24 11:09:34 -05:00
);
} else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.id = ?1
UNION
SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages JOIN messages_refs
ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1
`,
2024-02-24 11:09:34 -05:00
[this.hash.substring(1)]
);
} else if (this.hash.startsWith('##')) {
2025-04-09 18:50:14 -04:00
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
all_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.content ->> 'channel' = ?4
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
2025-04-09 18:50:14 -04:00
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
)
SELECT TRUE AS is_primary, all_news.* FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20
`,
[
JSON.stringify(this.following),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
);
2025-04-09 18:50:14 -04:00
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.sequence DESC LIMIT 20
`,
[JSON.stringify(this.private_messages), start_time, end_time]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else {
let t0 = new Date();
2025-04-09 18:50:14 -04:00
let initial_messages = await tfrpc.rpc.query(
`
WITH
all_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
),
news AS (
SELECT * FROM all_news
2025-03-30 13:18:16 -04:00
WHERE all_news.timestamp < ?3 AND (?2 IS NULL OR all_news.timestamp >= ?2)
ORDER BY timestamp DESC LIMIT 20
)
SELECT TRUE AS is_primary, news.* FROM news
`,
[JSON.stringify(this.following), start_time, end_time]
);
let t1 = new Date();
2025-04-09 18:50:14 -04:00
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
2025-04-09 18:50:14 -04:00
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
}
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
let only_primary = messages.filter((x) => x.is_primary);
this.time_range = [
only_primary.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
only_primary.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1]
),
];
}
async load_more() {
this.loading++;
this.loading_canceled = false;
try {
let more = [];
2025-01-22 18:28:55 -05:00
let last_start_time = this.time_range[0];
try {
more = await this.fetch_messages(null, last_start_time);
} catch (e) {
console.log(e);
}
2025-01-22 18:28:55 -05:00
this.update_time_range_from_messages(
more.filter((x) => x.timestamp < last_start_time)
2025-01-22 18:28:55 -05:00
);
this.messages = await this.decrypt([...more, ...this.messages]);
} finally {
this.loading--;
}
}
cancel_load() {
this.loading_canceled = true;
}
async decrypt(messages) {
let result = [];
for (let message of messages) {
let content;
try {
content = JSON.parse(message?.content);
2024-02-24 11:09:34 -05:00
} catch {}
if (typeof content === 'string') {
let decrypted;
try {
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
2024-02-24 11:09:34 -05:00
} catch {}
if (decrypted) {
try {
message.decrypted = JSON.parse(decrypted);
} catch {
message.decrypted = decrypted;
}
}
}
result.push(message);
}
return result;
}
merge_messages(old_messages, new_messages) {
let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
}
async load_latest() {
this.loading++;
let now = new Date().valueOf();
let end_time = now + 24 * 60 * 60 * 1000;
let messages = [];
try {
2025-01-27 21:02:15 -05:00
messages = await this.fetch_messages(this.time_range[0], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = this.merge_messages(
this.messages,
Object.values(
Object.fromEntries(
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
)
);
console.log('done loading latest messages.');
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
try {
if (this._messages_hash !== this.hash) {
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 = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(null, this.time_range[1]);
this.update_time_range_from_messages(
messages.filter((x) => x.timestamp < this.time_range[1])
);
messages = await this.decrypt(messages);
} finally {
this.loading--;
}
this.messages = this.merge_messages(this.messages, messages);
this.time_loading = undefined;
console.log(
`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
);
}
mark_all_read() {
2024-12-05 20:47:02 -05:00
let newest = this.messages.reduce(
(accumulator, current) => Math.max(accumulator, current.rowid),
this.channels_latest[this.channel()] ?? -1
2024-12-05 20:47:02 -05:00
);
if (newest >= 0) {
2024-12-05 20:47:02 -05:00
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel(),
unread: newest + 1,
},
})
);
}
}
render() {
2024-02-24 11:09:34 -05:00
if (
!this.messages ||
this._messages_hash !== this.hash ||
JSON.stringify(this._messages_following) !==
JSON.stringify(this.following)
2024-02-24 11:09:34 -05:00
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
this.load_messages();
}
let more;
if (!this.hash.startsWith('#%')) {
more = html`
<p>
2024-12-05 20:47:02 -05:00
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<button
?disabled=${this.loading}
class="w3-button w3-theme-d1"
@click=${this.load_more}
>
2024-02-24 11:09:34 -05:00
Load More
</button>
2024-12-05 20:47:02 -05:00
<button
class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
@click=${this.cancel_load}
>
Cancel
</button>
2024-12-05 20:47:02 -05:00
<span
>Showing
${new Date(
this.time_loading
? Math.min(this.time_loading[0], this.time_range[0])
: this.time_range[0]
).toLocaleDateString()}
-
${new Date(
this.time_loading
? Math.max(this.time_loading[1], this.time_range[1])
: this.time_range[1]
).toLocaleDateString()}.</span
2024-12-05 20:47:02 -05:00
>
</p>
`;
}
return cache(html`
2024-12-05 20:47:02 -05:00
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
2024-02-24 11:09:34 -05:00
<tf-news
id="news"
whoami=${this.whoami}
.users=${this.users}
.messages=${this.messages}
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
.recent_reactions=${this.recent_reactions}
2024-02-24 11:09:34 -05:00
></tf-news>
${more}
`);
}
}
2024-02-24 11:09:34 -05:00
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);