ssb: Merge in the new very work in progress channels interface.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m1s

This commit is contained in:
Cory McWilliams 2024-11-30 15:05:14 -05:00
parent 53044696ba
commit cd2c2587ae
6 changed files with 319 additions and 118 deletions

View File

@ -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}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@ -344,7 +370,7 @@ class TfElement extends LitElement {
};
let tabs = html`
<div class="w3-bar w3-theme-l1">
<div class="w3-bar w3-theme-l1" style="position: sticky; top: 0">
<button
class="w3-bar-item w3-button w3-circle w3-ripple"
@click=${this.refresh}
@ -385,12 +411,7 @@ class TfElement extends LitElement {
class="w3-theme-dark"
>
${tabs}
<div style="padding: 8px">
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
</div>
${contents}
</div>
`;
}

View File

@ -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`<p>To #${this.channel}:</p>` :
undefined}
${this.render_encrypt()}
<div class="w3-container w3-padding-small">
<div class="w3-half">

View File

@ -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)}</pre
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}`;
}
}
}
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)}</pre
}
let class_background = this.message?.decrypted
? 'w3-pale-red'
: 'w3-theme-d4';
: (this.message?.rowid >= 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)}</pre
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
`
)}
@ -442,6 +460,8 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}
</div>`;
@ -463,6 +483,8 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
`
)}
@ -618,6 +640,11 @@ ${JSON.stringify(content, null, 2)}</pre
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${!content.root ?
html`
<button class="w3-button w3-theme-d1" @click=${this.mark_read}>Set Read Here</button>
` :
undefined}
</p>
${this.render_children()}
</div>

View File

@ -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`
<div style="display: flex; flex-direction: column">
<div>
${final_messages.map(
(x) =>
html`<tf-message
@ -189,6 +192,8 @@ class TfNewsElement extends LitElement {
.drafts=${this.drafts}
.expanded=${this.expanded}
collapsed="true"
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}
</div>

View File

@ -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`
<p>
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
<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>
<span>Showing ${new Date(this.time_range[0]).toLocaleDateString()} - ${new Date(this.time_range[1]).toLocaleDateString()}.</span>
</p>
`;
}
@ -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()]}
></tf-news>
${more}
`;

View File

@ -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`<tf-profile
@ -129,39 +176,68 @@ class TfTabNewsElement extends LitElement {
</div>`;
}
return html`
<p class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
<div class="w3-sidebar w3-bar-block w3-theme-d1" style="width: 2in; left: 0" id="sidebar">
<div class="w3-right w3-button" @click=${this.hide_sidebar}>&times;</div>
${this.hash.startsWith('##') && this.channels.indexOf(this.hash.substring(2)) == -1 ?
html`
<div class="w3-bar-item w3-theme-d2">Viewing</div>
<a href="#" class="w3-bar-item w3-button" style="font-weight: bold">${this.hash.substring(2)}</a>
` :
undefined}
<div class="w3-bar-item w3-theme-d2">Channels</div>
<a href="#" class="w3-bar-item w3-button" style=${this.hash == '#' ? 'font-weight: bold' : undefined}>general ${this.unread_status('')}</a>
${this.channels.map(x => html`
<a
href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}>#${x} ${this.unread_status(x)}</a>
`)}
</div>
<div>
<tf-compose
id="tf-compose"
<div style="margin-left: 2in; padding: 8px" id="main">
<div id="show_sidebar" class="w3-left w3-button" style="display: none" @click=${this.show_sidebar}>&#9776;</div>
<p>
<button
class="w3-button w3-theme-d1"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
${this.hash.startsWith('##') ?
html`
<button class="w3-button w3-theme-d1" @click=${this.channel_toggle_subscribed}>
${this.channels.indexOf(this.hash.substring(2)) != -1 ? 'Unsubscribe from #' : 'Subscribe to #'}${this.hash.substring(2)}
</button>
` :
undefined}
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose>
</div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
></tf-compose>
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
></tf-tab-news-feed>
</div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
></tf-tab-news-feed>
`;
}
}