add reply

This commit is contained in:
tsukino
2023-12-28 00:21:42 -08:00
parent fca5b6e4bd
commit a9375d2fc6
11 changed files with 215 additions and 41 deletions

View File

@@ -8,6 +8,11 @@ export type UserProfileData = {
meta: { [k: string]: string };
};
export type PostMeta = {
moderations: { [subtype: string]: number };
replies: number;
};
export interface BaseDBAdapter {
insertMessage(message: Any): Promise<Any | null>;
getPosts(options?: {
@@ -42,10 +47,7 @@ export interface BaseDBAdapter {
},
): Promise<Any[]>;
getProfile(user: string): Promise<UserProfileData>;
getPostMeta(reference: string): Promise<{
moderations: { [subtype: string]: number };
replies: number;
}>;
getPostMeta(reference: string): Promise<PostMeta>;
getUserMeta(user: string): Promise<{
outgoingConnections: { [subtype: string]: number };
incomingConnections: { [subtype: string]: number };

View File

@@ -126,10 +126,14 @@ export default class LevelDBAdapter implements BaseDBAdapter {
const msg = message as Post;
if (msg.reference) {
await this.#indices.thread
.sublevel(msg.reference.split('/')[1], { valueEncoding: 'json' })
.sublevel(MessageType[msg.type], { valueEncoding: 'json' })
.put(time, message.hash);
const hash = msg.reference.split('/')[1];
if (hash) {
await this.#indices.thread
.sublevel(msg.reference.split('/')[1], { valueEncoding: 'json' })
.sublevel(MessageType[msg.type], { valueEncoding: 'json' })
.put(time, message.hash);
}
}
break;
@@ -319,8 +323,12 @@ export default class LevelDBAdapter implements BaseDBAdapter {
}
};
const hash = reference.split('/')[1] || reference;
if (!hash) return [];
const db = this.#indices.thread
.sublevel(reference.split('/')[1])
.sublevel(hash)
.sublevel(MessageType[MessageType.Post]);
return this.#query(db, predicate, options);
@@ -342,8 +350,12 @@ export default class LevelDBAdapter implements BaseDBAdapter {
);
};
const hash = reference.split('/')[1] || reference;
if (!hash) return [];
const db = this.#indices.thread
.sublevel(reference.split('/')[1])
.sublevel(hash)
.sublevel(MessageType[MessageType.Moderation]);
return this.#query(db, predicate, options);

View File

@@ -232,6 +232,22 @@ export class VNode {
}
}
if (newNode.classList.length) {
for (const className of newNode.classList) {
if (!lastEl.classList.contains(className)) {
lastEl.classList.add(className);
}
}
}
if (lastEl.classList.length) {
for (const className of Array.from(lastEl.classList)) {
if (!newNode.classList.includes(className)) {
lastEl.classList.remove(className);
}
}
}
if (dirty) {
lastEl.replaceWith(newNode.createElement());
return;

View File

@@ -1,9 +1,11 @@
@import "../Post/index.scss";
.editor {
.post {
grid-template-columns: auto auto;
grid-template-rows: auto auto auto auto;
background-color: var(--white);
&__post {
grid-template-columns: 4.5rem auto;
grid-template-rows: 4.5rem auto auto auto;
padding: 0;
profile-image {
@@ -12,16 +14,18 @@
grid-row-start: 1;
grid-row-end: 2;
--margin: 0 .5rem .5rem 0;
--box-shadow: var(--shadow);
padding: 1rem 0 0 1rem;
}
.top {
grid-column-start: 1;
display: flex;
flex-flow: column nowrap;
align-items: flex-start;
grid-column-start: 2;
grid-column-end: 3;
grid-row-start: 2;
grid-row-start: 1;
grid-row-end: 3;
padding: 0 1rem;
padding: 1rem 0;
}
.bottom {
@@ -45,7 +49,7 @@
background-color: var(--white);
font-size: var(--text-sm);
font-family: var(--font-sans);
margin: 0.5rem;
margin: 1rem .5rem .5rem;
border: 1px solid var(--slate-100);
border-radius: 4px;
resize: none;
@@ -89,4 +93,80 @@
}
}
}
post-card.parent {
--padding-top: 1rem;
--bottom-display: none;
--margin: 0 .5rem .5rem .5rem;
}
.ref {
display: block;
position: relative;
&--hidden {
display: none;
}
&__text {
&--cancel {
display: none;
}
&--reply {
display: block;
}
}
&__desc {
transition: width 200ms ease-in-out, padding 200ms ease-in-out;
display: flex;
flex-flow: row nowrap;
align-items: center;
cursor: pointer;
font-size: var(--text-sm);
margin-left: 4.5rem;
color: var(--blue-500);
font-weight: var(--font-medium);
.xmark {
width: 0;
margin-right: 0.25rem;
margin-top: 0.125rem;
flex: 0 0 auto;
border-radius: 1.25rem;
background: var(--red-100);
height: 1.25rem;
padding: 0.0625rem 0;
}
&:hover {
color: var(--red-500);
.xmark {
width: 1.25rem;
padding: 0.0625rem 0.125rem;
}
.ref__text {
&--reply {
display: none;
}
&--cancel {
display: block;
}
}
}
}
&__connector {
width: 2px;
background: var(--slate-100);
height: auto;
position: absolute;
top: 4.625rem;
left: 2.375rem;
bottom: -1rem;
}
}
}

View File

@@ -12,6 +12,7 @@ import { MessageType, Post, PostSubtype } from '@message';
import { Observable } from '../../../lib/state.ts';
import css from './index.scss';
import $editor from '../../state/editor.ts';
import XmarkIcon from '../../../static/icons/xmark.svg';
@connect(() => {
const content = new Observable('');
@@ -60,15 +61,42 @@ export default class Editor extends CustomElement {
const creator = this.state.creator;
const name = userName(creator) || 'Anonymous';
const handle = userId(creator) || '';
const [_c, _h] = $editor.reference.$.split('/');
const hash = _h || _c;
const parentCreator = _h ? _c : '';
const parentHandle = userId(parentCreator) || '';
return h(
'div.editor',
!!$editor.reference.$ &&
h('post-card', {
hash: $editor.reference.$,
}),
h(
'div.post',
'div.ref',
{
className: !hash ? 'ref--hidden' : '',
},
h('post-card.parent', {
hash: hash,
}),
h(
'div.ref__desc',
// @ts-ignore
{
onclick: () => {
$editor.reference.$ = '';
},
},
h('img.xmark', {
src: XmarkIcon,
}),
h(
'span.ref__text.ref__text--cancel',
`Cancel replying to ${parentHandle}`,
),
h('span.ref__text.ref__text--reply', `Replying to ${parentHandle}`),
),
h('div.ref__connector'),
),
h(
'div.post.editor__post',
h('profile-image', {
creator: creator,
}),

View File

@@ -1,7 +1,7 @@
.left-sidebar {
display: flex;
flex-flow: column nowrap;
width: 20rem;
width: 24rem;
flex: 1 0 auto;
gap: .25rem;

View File

@@ -2,11 +2,13 @@ import { connect, CustomElement, h, register } from '../../../lib/ui.ts';
import css from './index.scss';
import $signer from '../../state/signer.ts';
import '../Editor';
import { ProofType } from '@message';
import { Post, PostSubtype, ProofType } from '@message';
import $node from '../../state/node.ts';
import $editor from '../../state/editor.ts';
@connect(() => ({
ecdsa: $signer.$ecdsa,
reference: $editor.reference,
}))
export default class LeftSidebar extends CustomElement {
css = css.toString();
@@ -14,14 +16,20 @@ export default class LeftSidebar extends CustomElement {
onSubmit = async (e: CustomEvent) => {
const { post, reset } = e.detail;
if (post) {
const p = new Post({
...post.json,
subtype: $editor.reference.$ ? PostSubtype.Comment : PostSubtype.Default,
reference: $editor.reference.$ || '',
});
if (p) {
if ($signer.$ecdsa.$?.privateKey) {
post.commit({
p.commit({
type: ProofType.ECDSA,
value: $signer.$ecdsa.$.sign(post.hash),
value: $signer.$ecdsa.$.sign(p.hash),
});
await $node.node.publish(post);
await $node.node.publish(p);
reset();
}
}

View File

@@ -1,12 +1,14 @@
.post {
display: grid;
background: var(--white);
grid-template-columns: 3.5rem auto;
grid-template-rows: auto auto auto;
padding: .5rem;
display: var(--display, grid);
background: var(--background, var(--white));
grid-template-columns: var(--grid-template-columns, 3.5rem auto);
grid-template-rows: var(--grid-template-rows, auto auto auto);
padding: var(--padding, .5rem);
padding-top: var(--padding-top, .5rem);
margin: var(--margin, 0);
font-size: var(--font-size, var(--text-base));
border: var(--border, none);
cursor: default;
cursor: var(--cursor, default);
}
profile-image {
@@ -58,11 +60,12 @@ profile-image {
grid-row-end: 3;
padding: .25rem 0;
font-weight: var(--font-normal);
color: var(--slate-900);
line-height: 1.3125;
}
.bottom {
display: flex;
display: var(--bottom-display, flex);
flex-flow: row nowrap;
grid-column-start: 2;
grid-column-end: 3;

View File

@@ -19,10 +19,12 @@ import $editor from '../../state/editor.ts';
const hash = el.state.hash;
const post = $node.$posts.get(hash);
const user = $node.$users.get(post.$?.creator || '');
return {
post,
user,
reference: $editor.reference,
postmeta: $node.$postmetas.get(hash),
};
})
export default class Post extends CustomElement {
@@ -33,12 +35,17 @@ export default class Post extends CustomElement {
css = css.toString();
comment = () => {
$editor.reference.$ = this.state.hash;
const p = $node.$posts.get(this.state.hash);
$editor.reference.$ =
$editor.reference.$.split('/')[1] === this.state.hash
? ''
: p.$?.messageId || '';
};
render() {
const p = $node.$posts.get(this.state.hash);
const u = $node.$users.get(p.$?.creator || '');
const postmeta = $node.getPostMeta(this.state.hash);
const creator = p.$?.json.creator || '';
const createat = fromNow(p.$?.json.createdAt) || '';
@@ -46,6 +53,8 @@ export default class Post extends CustomElement {
const name = u.$?.name || userName(p.$?.json.creator) || 'Anonymous';
const handle = userId(p.$?.json.creator) || '';
const refHash = $editor.reference.$.split('/')[1] || $editor.reference.$;
return h(
'div.post',
h('profile-image', {
@@ -64,11 +73,11 @@ export default class Post extends CustomElement {
'c-button.comment-btn',
// @ts-ignore
{
...boolAttr('active', $editor.reference.$ === this.state.hash),
...boolAttr('active', refHash === this.state.hash),
onclick: this.comment,
},
h('img', { src: CommentIcon }),
h('span', '0'),
h('span', `${postmeta?.replies || 0}`),
),
h('c-button.repost-btn', h('img', { src: RepostIcon }), h('span', '0')),
h('c-button.like-btn', h('img', { src: LikeIcon }), h('span', '0')),

View File

@@ -1,7 +1,8 @@
import { Observable, ObservableMap } from '../../lib/state.ts';
import { Autism } from '@protocol/browser';
import { Post } from '@message';
import { UserProfileData } from '@autismjs/db/src/base.ts';
import { PostMeta, UserProfileData } from '@autismjs/db/src/base.ts';
import { equal } from '../utils/misc.ts';
export class NodeStore {
node: Autism;
@@ -10,11 +11,12 @@ export class NodeStore {
$globalPosts = new Observable<string[]>([]);
$posts = new ObservableMap<string, Post>();
$users = new ObservableMap<string, UserProfileData>();
$postmetas = new ObservableMap<string, PostMeta>();
constructor() {
const node = new Autism({
bootstrap: [
'/ip4/192.168.86.24/tcp/63482/ws/p2p/12D3KooWJ4guEVPUD1zLBvbevx7h5aJHXfN9xokhUMKj4dM2pvUG',
'/ip4/192.168.86.24/tcp/60336/ws/p2p/12D3KooWAeyUxK9NAudYufT2yadwDqK1KE5MTEYhkq2XMvzyFqs7',
],
});
@@ -57,6 +59,19 @@ export class NodeStore {
});
};
getPostMeta(messageId?: string) {
if (!messageId) return null;
const store = this.$postmetas.get(messageId);
this.node.db.db.getPostMeta(messageId).then((meta) => {
if (!equal(store.$, meta)) {
store.$ = meta;
}
});
return store.$;
}
getPost(hash: string) {
const store = this.$posts.get(hash);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#EC7063" height="16" width="12" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>

After

Width:  |  Height:  |  Size: 548 B