mirror of
https://github.com/autismjs/monorepo.git
synced 2026-01-09 17:17:55 -05:00
feat: adding post list
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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() || '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
49
packages/web/src/components/ProfileImage/index.ts
Normal file
49
packages/web/src/components/ProfileImage/index.ts
Normal 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);
|
||||
@@ -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,
|
||||
|
||||
14
packages/web/src/utils/misc.ts
Normal file
14
packages/web/src/utils/misc.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user