feat: adding post list

This commit is contained in:
tsukino
2023-12-22 19:28:59 -08:00
parent 49ccdcf218
commit 690f2675af
7 changed files with 196 additions and 39 deletions

View File

@@ -59,7 +59,7 @@ export type Subscription<ValueType = any> =
export class Observable<ObservableValue = any> {
#state: ObservableValue;
#error: Error | null = null;
#subscriptions: Subscription[] = [];
#subscriptions: Subscription<ObservableValue>[] = [];
get state() {
return this.#state;
@@ -101,10 +101,16 @@ export class Observable<ObservableValue = any> {
}
}
subscribe<ValueType = any>(subscription: Subscription<ValueType>) {
subscribe(subscription: Subscription<ObservableValue>) {
const currIndex = this.#subscriptions.indexOf(subscription);
if (currIndex === -1) {
this.#subscriptions.push(subscription);
const sub = subscription;
if (typeof sub === 'function') {
sub(this.state);
} else if (typeof sub !== 'function' && sub.next) {
sub.next(this.state);
}
}
return () => {
const index = this.#subscriptions.indexOf(subscription);
@@ -121,7 +127,7 @@ export class ObservableMap<keyType = string, ValueType = any> {
get(key: keyType) {
const exist = this.#map.get(key);
if (!exist) {
this.#map.set(key, new Observable(null));
this.#map.set(key, new Observable<ValueType | null>(null));
}
return this.#map.get(key);
}

View File

@@ -24,9 +24,22 @@ export function register(name: string, el: CustomElementConstructor) {
window.customElements.define(name, el);
}
export const Q = (root: ShadowRoot | Element) => {
return {
find: (str: string) => E(root.querySelector(str)),
export const Q = (root: ShadowRoot | Element | null) => {
if (!root) return root;
const q = {
el: root,
attr: (key: string, value: string) => {
if (root instanceof Element) {
root.setAttribute(key, value);
}
return q;
},
content: (content: string) => {
root.textContent = content;
return q;
},
find: (str: string) => Q(root.querySelector(str)),
findAll: (
str: string,
): Element[] & {
@@ -38,7 +51,7 @@ export const Q = (root: ShadowRoot | Element) => {
} => {
const list: any[] = Array.prototype.map.call(
root.querySelectorAll(str),
E,
Q,
);
// @ts-ignore
@@ -51,7 +64,7 @@ export const Q = (root: ShadowRoot | Element) => {
for (let i = 0; i < max; i++) {
const data = result[i];
const last = list[i];
const lastKey = last?.getAttribute('key');
const lastKey = last?.el?.getAttribute('key');
const currKey = mapKeyFn(data);
if (!last && data) {
@@ -68,21 +81,5 @@ export const Q = (root: ShadowRoot | Element) => {
return list;
},
};
};
const E = (el: Element | null) => {
if (!el) return el;
return {
el: el,
content: (content: string) => {
el.textContent = content;
return el;
},
attr: (key: string, value: string) => {
el.setAttribute(key, value);
return el;
},
getAttribute: el.getAttribute.bind(el),
};
return q;
};

View File

@@ -1,19 +1,79 @@
import { CustomElement, Q, register } from '../../../lib/ui.ts';
import { getStore } from '../../state';
import { default as NodeStore } from '../../state/node.ts';
import { userId, userName } from '../../utils/misc.ts';
import '../ProfileImage';
export default class Post extends CustomElement {
css = `
.post {
display: grid;
grid-template-columns: 3rem auto;
grid-template-rows: auto auto auto;
padding: .5rem;
grid-gap: .5rem;
font-size: var(--font-size, 15px);
}
profile-image {
--display: inline-block;
--width: 3rem;
--height: 3rem;
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 4;
}
.top {
display: flex;
flex-flow: row nowrap;
align-items: center;
grid-column-start: 2;
grid-column-end: 3;
grid-row-start: 1;
grid-row-end: 2;
gap: .25rem;
}
.creator {
font-weight: 700;
}
.userId {
color: var(--slate-300);
}
.content {
display: flex;
flex-flow: row nowrap;
grid-column-start: 2;
grid-column-end: 3;
grid-row-start: 2;
grid-row-end: 3;
}
.bottom {
display: flex;
flex-flow: row nowrap;
grid-column-start: 2;
grid-column-end: 3;
grid-row-start: 3;
grid-row-end: 4;
}
`;
html = `
<div class="post">
<div id="creator"></div>
<div id="content"></div>
<div id="createdAt"></div>
<profile-image></profile-image>
<div class="top">
<div class="creator"></div>
<div class="userId"></div>
</div>
<div class="content"></div>
<div class="bottom">
<div class="createdAt"></div>
</div>
</div>
`;
@@ -27,12 +87,18 @@ export default class Post extends CustomElement {
const node = store.get<NodeStore>('node');
const hash = this.dataset.hash!;
const post = await node.$posts.get(hash);
const q = Q(this.shadowRoot!);
const q = Q(this.shadowRoot!)!;
post!.subscribe((p) => {
q.find('div#creator')!.content(p.json.creator || '');
q.find('div#content')!.content(p.json.content || '');
q.find('div#createdAt')!.content(p.json.createdAt.toDateString() || '');
post!.subscribe(async (p) => {
const user = await node.node.db.db.getProfile(p!.json.creator || '');
const displayName = user.name || userName(p?.json.creator) || 'Anonymous';
const userHandle = userId(p?.json.creator);
q.find('profile-image')?.attr('address', p?.json.creator || '');
q.find('div.creator')!.content(displayName);
q.find('div.userId')!.content(userHandle || '');
q.find('div.content')!.content(p?.json.content || '');
q.find('div.createdAt')!.content(p?.json.createdAt.toDateString() || '');
});
}
}

View File

@@ -0,0 +1,49 @@
import { CustomElement, Q, register } from '../../../lib/ui.ts';
import { getStore } from '../../state';
import { default as NodeStore } from '../../state/node.ts';
export default class ProfileImage extends CustomElement {
static get observedAttributes() {
return ['address', 'width', 'height'];
}
css = `
img {
display: var(--display);
width: var(--width);
height: var(--height);
border-radius: 50%;
}
`;
html = `
<img />
`;
async connectedCallback() {
super.connectedCallback();
const address = this.getAttribute('address') || '';
const width = this.getAttribute('width') || '';
const height = this.getAttribute('height') || '';
this.attributeChangedCallback('address', '', address);
this.attributeChangedCallback('width', '', width);
this.attributeChangedCallback('height', '', height);
}
attributeChangedCallback = (key: string, oldVal: string, newVal: string) => {
const store = getStore();
const node: NodeStore = store.get('node');
const img = Q(this.shadowRoot!)!.find('img')!;
img.attr(key, newVal);
if (key === 'address') {
img.attr(
'src',
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAABNZJREFUeF7tnFmW6jAMRJP975Ut0CcNnc5gR6XRcmL+HtFQqitDMzzm1+v1nsbN1oF5nqa3zNZ5ALFlUao2T9OE4ukXCGdKynNOLU4s0bdUigZSE2AojPIr+3VLK1YglkWzG5hZH31CMquvaku4XqAkNZD1DwqwoZ4v2AgM0+vRVDiLVANZ5HQxO+ib7yx0dRMg66x0P9AWfljD1nyxFxm2QEylIcW0GLT5lEZ+fRAIvzAl9enXq68moFfqFA/qemL3s0m/PCHZxCbmei2NYST4kAVawWgMVgTCbJqqq5AF/gOuQm2BCN67ARz3DyHNpCUYlPhtEgqEHksasdix3ND3VKV9/PPcgGAbU48qXcFq+pvm2cENCCb6CRZjTvxFNQbCE9t1NLh7sUBAUWmN3+j3GsUByDzN0/vw9OolPyE65qjHcDUQZn9jB9t2Nx6mzz97QxGENvvgLZwQZxVG5Y3KeCy5qmbxIQsaFgpSacub7DX7fJtX6nnZkcoOcNVP6mTDEcByYABh2aULRh7p3IBcN0ekgcMblgI74mECbW5ASNWnV0R+b9ZyfeHGk7MyAphAFFIVqYx5DEIpodR13feimEAM5tWWAPwgW1jUIJvIAkRA/OfRdEBz0biNsVTK9joVW+H1BSLMli0BkQVoAUJcpAUUFZ2QAF2PbWEE5MYrG7waRkBiVMPY4UCmbq+626cp6JuLTN2PCC/CqRNDWXZ1Qp4A+jZA0A1EoFrWQvptYxoAaTku1574+HggCXhIJEhyJP+1LB7IcekEkwpS4lcd6bgb5PMPGsiaJLChkCKogox2mxgayG1GLQ9iuiCaYsvn6e/xmXrQuuGkHnJCcEP4hGxr3wAIaEjhCZRvvjIDkEoAOVQACtKSTYrQbTqNAE/IMDGKLwjERk7XWIPEhwKxwdp3FYrrAJKM7wYIxa6knJfDi07m1EmOzzTjhDTgfvWjpWcgbPDsBDsLGra2G2JfiTwhN5zZy0tR3aO/JJC1yyCj+44oiAsHon9OByUlCGu4fDogCby7k4RlD3yBWGyaRY2OqPkCuTCi7PNy73LL96s+UXvRDEgfS2uFAa/jCAQX0QccnUrUDR4QtKpOO/uBzq1dg8JlIFfGN4fi6ZLzcED5IpDLPKCoh2WN2tZHcRJ08ZDF6ciJ9cDlVPM7VuR0vOcQzdyRU2l0Vt+RiBkgDoi1Sc3qlcBs76uAA3kOIJ5gQQhbCQOIJxBB7fZABFskmLObFCEQwkVjk5FySMyJiijJl+0eSEKBvuPnqy48IT6DxO9DfEfKuVRAKLF3vv63Gu5A8u1gbqyOQAYKCfpfICfr1jsiTV0PbcpPDCXmSnLMT0gkQsnAa06EUEEPcyAqk0ay37dOBMvRLQ75rOfMfCdEPp3rh0n/sgoCDTXnA9LtOQGFb776Xnwjv/R7WYbAaZVOzZzK0vNUIlA96hOCNhJPok1ML3A/oBqI1q+m+QlhNQWS0A96P5xFxwNxHoh29CpCKa6ajtVdotyBYFJ0Nt4pmwckm7vZ9BQ3gyeSB6TBKvLGaSDQuGV6IMbzfsq5U5Y3UAGRt3Wx+VzUU6CoNp2kAhJkq28b2iNGf30xBZBK8+/d+6t6oQxXug5VADnObW+6fcX8rPhAnuhSIEc+kEBxT2z1A58ObwvXw1MMAAAAAElFTkSuQmCC',
);
}
};
}
register('profile-image', ProfileImage);

View File

@@ -1,7 +1,6 @@
import { CustomElement, html, Q, register } from '../../../lib/ui.ts';
import { getStore } from '../../state';
import { default as NodeState } from '../../state/node.ts';
import { Post as PostType } from '@autismjs/message';
import '../../components/Post';
export default class App extends CustomElement {
@@ -10,11 +9,20 @@ export default class App extends CustomElement {
width: 100vw;
height: 100vh;
}
.posts {
width: 100vw;
height: 100vh;
display: flex;
flex-flow: column nowrap;
gap: 0.25rem;
padding: 0.25rem;
}
`;
html = `
<div class="app">
<slot></slot>
<div class="posts" />
</div>
`;
@@ -26,10 +34,9 @@ export default class App extends CustomElement {
subscribeToPosts() {
const state = getStore();
const node = state.get<NodeState>('node');
node.$globalPosts.subscribe<string[]>((hashes) => {
const app = this.shadowRoot!.querySelector('div.app')!;
const q = Q(app);
const old = q.findAll('post-card');
node.$globalPosts.subscribe((hashes) => {
const app = Q(this.shadowRoot!)!.find('div.posts')!;
const old = Q(app.el)!.findAll('post-card');
old.patch(
hashes,

View File

@@ -0,0 +1,14 @@
export function userId(pubkey?: string) {
if (!pubkey) return null;
return '@' + ellipsify(pubkey);
}
export function userName(pubkey?: string) {
if (!pubkey) return null;
return ellipsify(pubkey);
}
export function ellipsify(pubkey?: string, start = 6, end = 4) {
if (!pubkey) return null;
return pubkey.slice(0, start) + '...' + pubkey.slice(-end);
}

View File

@@ -4,7 +4,25 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auti.sm</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet">
<style>
html {
--white: #ffffff;
--slate-100: #EAECEE;
--slate-200: #D5D8DC;
--slate-300: #ABB2B9;
--slate-400: #808B96;
--slate-500: #566573;
--slate-600: #2C3E50;
--slate-700: #273746;
--slate-800: #212F3D;
--slate-900: #1C2833;
--black: #000000;
font-family: "Open Sans", sans-serif;
}
body {
margin: 0;
padding: 0;