rendering message

This commit is contained in:
balzack 2024-12-06 23:36:36 -08:00
parent 2e170f693f
commit c53bfeb039
13 changed files with 161 additions and 31 deletions

View File

@ -36,6 +36,14 @@
width: 100%; width: 100%;
} }
.spinner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.add { .add {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;

View File

@ -3,7 +3,7 @@ import { Focus } from 'databag-client-sdk'
import classes from './Conversation.module.css' import classes from './Conversation.module.css'
import { useConversation } from './useConversation.hook'; import { useConversation } from './useConversation.hook';
import { IconX } from '@tabler/icons-react' import { IconX } from '@tabler/icons-react'
import { Text } from '@mantine/core' import { Text, Loader } from '@mantine/core'
import { Message } from '../message/Message'; import { Message } from '../message/Message';
export type MediaAsset = { export type MediaAsset = {
@ -46,9 +46,16 @@ export function Conversation() {
<IconX size={24} className={classes.close} onClick={actions.close} /> <IconX size={24} className={classes.close} onClick={actions.close} />
</div> </div>
<div className={classes.frame}> <div className={classes.frame}>
<div className={classes.thread}> { !state.loaded && (
{topics} <div className={classes.spinner}>
</div> <Loader size={64} />
</div>
)}
{ state.loaded && (
<div className={classes.thread}>
{topics}
</div>
)}
</div> </div>
<div className={classes.add}> <div className={classes.add}>
<input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/> <input type='file' name="asset" accept="image/*" ref={attachImage} onChange={e => onSelectImage(e)} style={{display: 'none'}}/>

View File

@ -14,6 +14,7 @@ export function useConversation() {
layout: null, layout: null,
strings: display.state.strings, strings: display.state.strings,
topics: [] as Topic[], topics: [] as Topic[],
loaded: false,
profile: null as Profile | null, profile: null as Profile | null,
cards: new Map<string, Card>(), cards: new Map<string, Card>(),
host: false, host: false,
@ -34,16 +35,18 @@ export function useConversation() {
const { contact, identity } = app.state.session || { }; const { contact, identity } = app.state.session || { };
if (focus && contact && identity) { if (focus && contact && identity) {
const setTopics = (topics: Topic[]) => { const setTopics = (topics: Topic[]) => {
const sorted = topics.sort((a, b) => { if (topics) {
if (a.created < b.created) { const sorted = topics.sort((a, b) => {
return -1; if (a.created < b.created) {
} else if (a.created > b.created) { return -1;
return 1; } else if (a.created > b.created) {
} else { return 1;
return 0; } else {
} return 0;
}); }
updateState({ topics: sorted }); });
updateState({ topics: sorted, loaded: true });
}
} }
const setCards = (cards: Card[]) => { const setCards = (cards: Card[]) => {
const contacts = new Map<string, Card>(); const contacts = new Map<string, Card>();
@ -63,6 +66,7 @@ export function useConversation() {
} }
console.log(focused); console.log(focused);
} }
updateState({ topics: [], loaded: false });
focus.addTopicListener(setTopics); focus.addTopicListener(setTopics);
focus.addDetailListener(setDetail); focus.addDetailListener(setDetail);
contact.addCardListener(setCards); contact.addCardListener(setCards);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -4,21 +4,52 @@
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;
.media {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.goleft {
position: absolute;
left: 32px;
}
.goright {
position: absolute;
right: 32px;
}
}
.assets { .assets {
width: 100%; width: 100%;
height: 128px; height: 128px;
padding-left: 72px; padding-left: 72px;
padding-right: 32px; padding-right: 32px;
margin-top: 8px; margin-top: 8px;
display: flex;
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;
gap: 16px;
min-width: 0; min-width: 0;
margin-bottom: 8px; margin-bottom: 8px;
overflow: auto; overflow: auto;
-ms-overflow-style: none;
scrollbar-width: none;
display: flex;
flex-direction: row;
} }
.assets::-webkit-scrollbar {
display: none;
}
.thumbs {
display: flex;
flex-direction: row;
align-items: center;
flex-grow: 1;
gap: 16px;
}
.failed { .failed {
width: 64px; width: 64px;
height: 64px; height: 64px;

View File

@ -1,7 +1,8 @@
import { useRef, useState, useCallback } from 'react';
import { avatar } from '../constants/Icons' import { avatar } from '../constants/Icons'
import { Topic, Card, Profile } from 'databag-client-sdk'; import { Topic, Card, Profile } from 'databag-client-sdk';
import classes from './Message.module.css' import classes from './Message.module.css'
import { Image, Skeleton } from '@mantine/core' import { Image, Skeleton, ActionIcon } from '@mantine/core'
import { ImageAsset } from './imageAsset/ImageAsset'; import { ImageAsset } from './imageAsset/ImageAsset';
import { AudioAsset } from './audioAsset/AudioAsset'; import { AudioAsset } from './audioAsset/AudioAsset';
import { VideoAsset } from './videoAsset/VideoAsset'; import { VideoAsset } from './videoAsset/VideoAsset';
@ -9,10 +10,12 @@ import { BinaryAsset } from './binaryAsset/BinaryAsset';
import type { MediaAsset } from '../conversation/Conversation'; import type { MediaAsset } from '../conversation/Conversation';
import { useMessage } from './useMessage.hook'; import { useMessage } from './useMessage.hook';
import failed from '../images/failed.png' import failed from '../images/failed.png'
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
import { useResizeDetector } from 'react-resize-detector';
export function Message({ topic, card, profile, host }: { topic: Topic, card: Card | null, profile: Profile | null, host: boolean }) { export function Message({ topic, card, profile, host }: { topic: Topic, card: Card | null, profile: Profile | null, host: boolean }) {
const { state, actions } = useMessage(); const { state, actions } = useMessage();
const scroll = useRef(null as HTMLDivElement | null);
const { locked, data, created, topicId, status, transform } = topic; const { locked, data, created, topicId, status, transform } = topic;
const { name, handle, node } = profile || card || { name: null, handle: null, node: null } const { name, handle, node } = profile || card || { name: null, handle: null, node: null }
const { text, textColor, textSize, assets } = data || { text: null, textColor: null, textSize: null } const { text, textColor, textSize, assets } = data || { text: null, textColor: null, textSize: null }
@ -20,6 +23,22 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
const logoUrl = profile ? profile.imageUrl : card ? card.imageUrl : avatar; const logoUrl = profile ? profile.imageUrl : card ? card.imageUrl : avatar;
const timestamp = actions.getTimestamp(created); const timestamp = actions.getTimestamp(created);
const [showScroll, setShowScroll] = useState(false);
const onResize = useCallback(() => {
setShowScroll(scroll.current ? scroll.current.scrollWidth > scroll.current.clientWidth : false);
}, []);
const { ref } = useResizeDetector({ onResize });
const scrollLeft = () => {
if (scroll.current) {
scroll.current.scrollTo({ top: 0, left: scroll.current.scrollLeft+92, behavior: 'smooth' });
}
}
const scrollRight = () => {
if (scroll.current) {
scroll.current.scrollTo({ top: 0, left: scroll.current.scrollLeft-92, behavior: 'smooth' });
}
}
const options = []; const options = [];
const media = !assets ? [] : assets.map((asset: MediaAsset, index: number) => { const media = !assets ? [] : assets.map((asset: MediaAsset, index: number) => {
@ -29,11 +48,13 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
return <AudioAsset key={index} topicId={topicId} asset={asset as MediaAsset} /> return <AudioAsset key={index} topicId={topicId} asset={asset as MediaAsset} />
} else if (asset.video || asset.encrypted?.type === 'video') { } else if (asset.video || asset.encrypted?.type === 'video') {
return <VideoAsset key={index} topicId={topicId} asset={asset as MediaAsset} /> return <VideoAsset key={index} topicId={topicId} asset={asset as MediaAsset} />
} else if (asset.binary || asset.encrypted?.type === 'binary') {
return <BinaryAsset key={index} topicId={topicId} asset={asset as MediaAsset} />
} else { } else {
return <></> return <div key={index}></div>
} }
}); });
return ( return (
<div className={classes.topic}> <div className={classes.topic}>
<div className={classes.content}> <div className={classes.content}>
@ -71,8 +92,26 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
</div> </div>
</div> </div>
{ !locked && media.length > 0 && transform === 'complete' && ( { !locked && media.length > 0 && transform === 'complete' && (
<div className={classes.assets}> <div className={classes.media}>
{ media } <div ref={scroll} className={classes.assets}>
<div ref={ref} className={classes.thumbs}>
{ media }
</div>
</div>
{ showScroll && (
<div className={classes.goleft}>
<ActionIcon variant="light" onClick={scrollLeft}>
<IconChevronLeft size={18} stroke={1.5} />
</ActionIcon>
</div>
)}
{ showScroll && (
<div className={classes.goright}>
<ActionIcon variant="light" onClick={scrollRight}>
<IconChevronRight size={18} stroke={1.5} />
</ActionIcon>
</div>
)}
</div> </div>
)} )}
{ !locked && media.length > 0 && transform === 'incomplete' && ( { !locked && media.length > 0 && transform === 'incomplete' && (

View File

@ -1,8 +1,21 @@
.asset { .asset {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.label {
position: absolute;
top: 0;
width: 100%;
overflow: hidden;
text-align: center;
}
.thumb { .thumb {
width: auto; width: auto;
height: 128px; height: 128px;
border-radius: 4px;
} }
} }

View File

@ -7,9 +7,12 @@ import { Image } from '@mantine/core'
export function AudioAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { export function AudioAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) {
const { state, actions } = useAudioAsset(topicId, asset); const { state, actions } = useAudioAsset(topicId, asset);
const { label } = asset.encrypted || asset.audio;
return ( return (
<div className={classes.asset}> <div className={classes.asset}>
<Image className={classes.thumb} src={audio} fit="contain" /> <Image className={classes.thumb} src={audio} fit="contain" />
<div className={classes.label}>{ label }</div>
</div> </div>
) )
} }

View File

@ -1,8 +1,27 @@
.asset { .asset {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.label {
position: absolute;
top: 0;
display: flex;
align-items: center;
flex-direction: column;
width: 100%;
overflow: hidden;
}
.extension {
font-weight: bold;
}
.thumb { .thumb {
width: auto; width: auto;
height: 128px; height: 128px;
border-radius: 4px;
} }
} }

View File

@ -7,9 +7,14 @@ import { Image } from '@mantine/core'
export function BinaryAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) { export function BinaryAsset({ topicId, asset }: { topicId: string, asset: MediaAsset }) {
const { state, actions } = useBinaryAsset(topicId, asset); const { state, actions } = useBinaryAsset(topicId, asset);
const { label, extension } = asset.encrypted || asset.binary;
return ( return (
<div className={classes.asset}> <div className={classes.asset}>
<Image className={classes.thumb} src={binary} fit="contain" /> <Image className={classes.thumb} src={binary} fit="contain" />
<div className={classes.label}>
<div>{ label }</div>
<div className={classes.extension}>{ extension }</div>
</div>
</div> </div>
) )
} }

View File

@ -135,8 +135,8 @@ export interface Focus {
setBlockTopic(topicId: string): Promise<void>; setBlockTopic(topicId: string): Promise<void>;
clearBlockTopic(topicId: string): Promise<void>; clearBlockTopic(topicId: string): Promise<void>;
addTopicListener(ev: (topics: Topic[]) => void): void; addTopicListener(ev: (topics: null | Topic[]) => void): void;
removeTopicListener(ev: (topics: Topic[]) => void): void; removeTopicListener(ev: (topics: null | Topic[]) => void): void;
addDetailListener(ev: (focused: { cardId: string | null, channelId: string, detail: FocusDetail | null }) => void): void; addDetailListener(ev: (focused: { cardId: string | null, channelId: string, detail: FocusDetail | null }) => void): void;
removeDetailListener(ev: (focused: { cardId: string | null, channelId: string, detail: FocusDetail | null }) => void): void; removeDetailListener(ev: (focused: { cardId: string | null, channelId: string, detail: FocusDetail | null }) => void): void;

View File

@ -51,6 +51,7 @@ export class FocusModule implements Focus {
private markRead: ()=>Promise<void>; private markRead: ()=>Promise<void>;
private flagChannelTopic: (id: string)=>Promise<void>; private flagChannelTopic: (id: string)=>Promise<void>;
private focusDetail: FocusDetail | null; private focusDetail: FocusDetail | null;
private loaded: boolean;
private markers: Set<string>; private markers: Set<string>;
@ -92,6 +93,7 @@ export class FocusModule implements Focus {
const { guid } = this; const { guid } = this;
this.nextRevision = revision; this.nextRevision = revision;
this.storeView = await this.getChannelTopicRevision(); this.storeView = await this.getChannelTopicRevision();
this.localComplete = this.storeView.revision == null;
// load markers // load markers
const values = await this.store.getMarkers(guid); const values = await this.store.getMarkers(guid);
@ -99,7 +101,6 @@ export class FocusModule implements Focus {
this.markers.add(value); this.markers.add(value);
}); });
this.emitTopics();
this.unsealAll = true; this.unsealAll = true;
this.loadMore = true; this.loadMore = true;
this.syncing = false; this.syncing = false;
@ -162,6 +163,7 @@ export class FocusModule implements Focus {
} else { } else {
this.loadMore = false; this.loadMore = false;
} }
await this.markRead();
this.emitTopics(); this.emitTopics();
} catch (err) { } catch (err) {
this.log.warn(err); this.log.warn(err);
@ -744,9 +746,6 @@ export class FocusModule implements Focus {
throw new Error('topic entry not found'); throw new Error('topic entry not found');
} }
const { assets } = this.getTopicData(entry.item); const { assets } = this.getTopicData(entry.item);
console.log(">>> ", assetId, entry.item, assets);
const asset = assets.find(item => item.assetId === assetId); const asset = assets.find(item => item.assetId === assetId);
if (!asset) { if (!asset) {
throw new Error('asset entry not found'); throw new Error('asset entry not found');
@ -819,17 +818,18 @@ console.log(">>> ", assetId, entry.item, assets);
await this.sync(); await this.sync();
} }
public addTopicListener(ev: (topics: Topic[]) => void) { public addTopicListener(ev: (topics: null | Topic[]) => void) {
this.emitter.on('topic', ev); this.emitter.on('topic', ev);
const topics = Array.from(this.topicEntries, ([topicId, entry]) => entry.topic); const topics = this.loaded ? Array.from(this.topicEntries, ([topicId, entry]) => entry.topic) : null;
ev(topics); ev(topics);
} }
public removeTopicListener(ev: (topics: Topic[]) => void) { public removeTopicListener(ev: (topics: | Topic[]) => void) {
this.emitter.off('topic', ev); this.emitter.off('topic', ev);
} }
private emitTopics() { private emitTopics() {
this.loaded = true;
const topics = Array.from(this.topicEntries, ([topicId, entry]) => entry.topic); const topics = Array.from(this.topicEntries, ([topicId, entry]) => entry.topic);
this.emitter.emit('topic', topics); this.emitter.emit('topic', topics);
} }

View File

@ -395,6 +395,7 @@ export class StreamModule {
const markRead = async () => { const markRead = async () => {
try { try {
console.log("MARKING AS READ!!!");
await this.setUnreadChannel(channelId, false); await this.setUnreadChannel(channelId, false);
} catch (err) { } catch (err) {
this.log.error('failed to mark as read'); this.log.error('failed to mark as read');