diff --git a/packages/db/src/base.ts b/packages/db/src/base.ts index 721121e..5f71b49 100644 --- a/packages/db/src/base.ts +++ b/packages/db/src/base.ts @@ -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; getPosts(options?: { @@ -42,10 +47,7 @@ export interface BaseDBAdapter { }, ): Promise; getProfile(user: string): Promise; - getPostMeta(reference: string): Promise<{ - moderations: { [subtype: string]: number }; - replies: number; - }>; + getPostMeta(reference: string): Promise; getUserMeta(user: string): Promise<{ outgoingConnections: { [subtype: string]: number }; incomingConnections: { [subtype: string]: number }; diff --git a/packages/db/src/level.ts b/packages/db/src/level.ts index c1e17c0..3b71ef8 100644 --- a/packages/db/src/level.ts +++ b/packages/db/src/level.ts @@ -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); diff --git a/packages/web/lib/ui.ts b/packages/web/lib/ui.ts index b29caa9..92aa13a 100644 --- a/packages/web/lib/ui.ts +++ b/packages/web/lib/ui.ts @@ -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; diff --git a/packages/web/src/components/Editor/index.scss b/packages/web/src/components/Editor/index.scss index db38274..63ddb7d 100644 --- a/packages/web/src/components/Editor/index.scss +++ b/packages/web/src/components/Editor/index.scss @@ -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; + } + } } \ No newline at end of file diff --git a/packages/web/src/components/Editor/index.ts b/packages/web/src/components/Editor/index.ts index bd5717c..f405581 100644 --- a/packages/web/src/components/Editor/index.ts +++ b/packages/web/src/components/Editor/index.ts @@ -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, }), diff --git a/packages/web/src/components/LeftSidebar/index.scss b/packages/web/src/components/LeftSidebar/index.scss index 4346fe6..cf9be0e 100644 --- a/packages/web/src/components/LeftSidebar/index.scss +++ b/packages/web/src/components/LeftSidebar/index.scss @@ -1,7 +1,7 @@ .left-sidebar { display: flex; flex-flow: column nowrap; - width: 20rem; + width: 24rem; flex: 1 0 auto; gap: .25rem; diff --git a/packages/web/src/components/LeftSidebar/index.ts b/packages/web/src/components/LeftSidebar/index.ts index 2bdeb98..3b55167 100644 --- a/packages/web/src/components/LeftSidebar/index.ts +++ b/packages/web/src/components/LeftSidebar/index.ts @@ -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(); } } diff --git a/packages/web/src/components/Post/index.scss b/packages/web/src/components/Post/index.scss index a57a3a6..14b41af 100644 --- a/packages/web/src/components/Post/index.scss +++ b/packages/web/src/components/Post/index.scss @@ -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; diff --git a/packages/web/src/components/Post/index.ts b/packages/web/src/components/Post/index.ts index 86150ec..2bc132b 100644 --- a/packages/web/src/components/Post/index.ts +++ b/packages/web/src/components/Post/index.ts @@ -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')), diff --git a/packages/web/src/state/node.ts b/packages/web/src/state/node.ts index c1ad7f8..30ef63d 100644 --- a/packages/web/src/state/node.ts +++ b/packages/web/src/state/node.ts @@ -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([]); $posts = new ObservableMap(); $users = new ObservableMap(); + $postmetas = new ObservableMap(); 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); diff --git a/packages/web/static/icons/xmark.svg b/packages/web/static/icons/xmark.svg new file mode 100644 index 0000000..7eb57ea --- /dev/null +++ b/packages/web/static/icons/xmark.svg @@ -0,0 +1 @@ + \ No newline at end of file