forked from cory/tildefriends
.gitea
apps
admin
api
apps
blog
db
follow
identity
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
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
462 lines
14 KiB
JavaScript
462 lines
14 KiB
JavaScript
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},
|
|
};
|
|
}
|
|
|
|
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 = {};
|
|
this.start_time = new Date().valueOf();
|
|
this.time_range = [0, 0];
|
|
this.time_loading = undefined;
|
|
this.loading = 0;
|
|
}
|
|
|
|
channel() {
|
|
return this.hash.startsWith('##')
|
|
? this.hash.substring(2)
|
|
: this.hash.substring(1);
|
|
}
|
|
|
|
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))]
|
|
);
|
|
let t0 = new Date();
|
|
let result = [].concat(
|
|
combined,
|
|
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 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'
|
|
`,
|
|
[
|
|
JSON.stringify(this.following),
|
|
JSON.stringify(refs2.map((x) => x.ref)),
|
|
]
|
|
)
|
|
);
|
|
let t1 = new Date();
|
|
console.log((t1 - t0) / 1000);
|
|
return result;
|
|
}
|
|
|
|
async fetch_messages(start_time, end_time) {
|
|
this.time_loading = [start_time, end_time];
|
|
let result;
|
|
if (this.hash == '#@') {
|
|
result = await tfrpc.rpc.query(
|
|
`
|
|
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
|
|
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
|
|
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
|
|
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
|
|
`,
|
|
[
|
|
'"' + this.whoami.replace('"', '""') + '"',
|
|
JSON.stringify(this.following),
|
|
start_time,
|
|
end_time,
|
|
]
|
|
);
|
|
} else if (this.hash.startsWith('#@')) {
|
|
result = await tfrpc.rpc.query(
|
|
`
|
|
WITH
|
|
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
|
FROM messages
|
|
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]
|
|
);
|
|
} 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
|
|
`,
|
|
[this.hash.substring(1)]
|
|
);
|
|
} else if (this.hash.startsWith('##')) {
|
|
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
|
|
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('"', '""') + '"',
|
|
]
|
|
);
|
|
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();
|
|
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
|
|
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();
|
|
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}`
|
|
);
|
|
}
|
|
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 = [];
|
|
let last_start_time = this.time_range[0];
|
|
try {
|
|
more = await this.fetch_messages(null, last_start_time);
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
this.update_time_range_from_messages(
|
|
more.filter((x) => x.timestamp < last_start_time)
|
|
);
|
|
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);
|
|
} catch {}
|
|
if (typeof content === 'string') {
|
|
let decrypted;
|
|
try {
|
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
|
} 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 {
|
|
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() {
|
|
let newest = this.messages.reduce(
|
|
(accumulator, current) => Math.max(accumulator, current.rowid),
|
|
this.channels_latest[this.channel()] ?? -1
|
|
);
|
|
if (newest >= 0) {
|
|
this.dispatchEvent(
|
|
new CustomEvent('channelsetunread', {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: {
|
|
channel: this.channel(),
|
|
unread: newest + 1,
|
|
},
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (
|
|
!this.messages ||
|
|
this._messages_hash !== this.hash ||
|
|
JSON.stringify(this._messages_following) !==
|
|
JSON.stringify(this.following)
|
|
) {
|
|
console.log(
|
|
`loading messages for ${this.whoami} (following ${this.following.length})`
|
|
);
|
|
this.load_messages();
|
|
}
|
|
let more;
|
|
if (!this.hash.startsWith('#%')) {
|
|
more = html`
|
|
<p>
|
|
<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}
|
|
>
|
|
Load More
|
|
</button>
|
|
<button
|
|
class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
|
|
@click=${this.cancel_load}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<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
|
|
>
|
|
</p>
|
|
`;
|
|
}
|
|
return cache(html`
|
|
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
|
|
Mark All Read
|
|
</button>
|
|
<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()]}
|
|
></tf-news>
|
|
${more}
|
|
`);
|
|
}
|
|
}
|
|
|
|
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|