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%;
}
.spinner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.add {
display: flex;
flex-shrink: 0;

View File

@ -3,7 +3,7 @@ import { Focus } from 'databag-client-sdk'
import classes from './Conversation.module.css'
import { useConversation } from './useConversation.hook';
import { IconX } from '@tabler/icons-react'
import { Text } from '@mantine/core'
import { Text, Loader } from '@mantine/core'
import { Message } from '../message/Message';
export type MediaAsset = {
@ -46,9 +46,16 @@ export function Conversation() {
<IconX size={24} className={classes.close} onClick={actions.close} />
</div>
<div className={classes.frame}>
<div className={classes.thread}>
{topics}
</div>
{ !state.loaded && (
<div className={classes.spinner}>
<Loader size={64} />
</div>
)}
{ state.loaded && (
<div className={classes.thread}>
{topics}
</div>
)}
</div>
<div className={classes.add}>
<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,
strings: display.state.strings,
topics: [] as Topic[],
loaded: false,
profile: null as Profile | null,
cards: new Map<string, Card>(),
host: false,
@ -34,16 +35,18 @@ export function useConversation() {
const { contact, identity } = app.state.session || { };
if (focus && contact && identity) {
const setTopics = (topics: Topic[]) => {
const sorted = topics.sort((a, b) => {
if (a.created < b.created) {
return -1;
} else if (a.created > b.created) {
return 1;
} else {
return 0;
}
});
updateState({ topics: sorted });
if (topics) {
const sorted = topics.sort((a, b) => {
if (a.created < b.created) {
return -1;
} else if (a.created > b.created) {
return 1;
} else {
return 0;
}
});
updateState({ topics: sorted, loaded: true });
}
}
const setCards = (cards: Card[]) => {
const contacts = new Map<string, Card>();
@ -63,6 +66,7 @@ export function useConversation() {
}
console.log(focused);
}
updateState({ topics: [], loaded: false });
focus.addTopicListener(setTopics);
focus.addDetailListener(setDetail);
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%;
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 {
width: 100%;
height: 128px;
padding-left: 72px;
padding-right: 32px;
margin-top: 8px;
display: flex;
flex-grow: 1;
flex-shrink: 1;
gap: 16px;
min-width: 0;
margin-bottom: 8px;
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 {
width: 64px;
height: 64px;

View File

@ -1,7 +1,8 @@
import { useRef, useState, useCallback } from 'react';
import { avatar } from '../constants/Icons'
import { Topic, Card, Profile } from 'databag-client-sdk';
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 { AudioAsset } from './audioAsset/AudioAsset';
import { VideoAsset } from './videoAsset/VideoAsset';
@ -9,10 +10,12 @@ import { BinaryAsset } from './binaryAsset/BinaryAsset';
import type { MediaAsset } from '../conversation/Conversation';
import { useMessage } from './useMessage.hook';
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 }) {
const { state, actions } = useMessage();
const scroll = useRef(null as HTMLDivElement | null);
const { locked, data, created, topicId, status, transform } = topic;
const { name, handle, node } = profile || card || { name: null, handle: null, node: 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 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 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} />
} else if (asset.video || asset.encrypted?.type === 'video') {
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 {
return <></>
return <div key={index}></div>
}
});
return (
<div className={classes.topic}>
<div className={classes.content}>
@ -71,8 +92,26 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
</div>
</div>
{ !locked && media.length > 0 && transform === 'complete' && (
<div className={classes.assets}>
{ media }
<div className={classes.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>
)}
{ !locked && media.length > 0 && transform === 'incomplete' && (

View File

@ -1,8 +1,21 @@
.asset {
display: flex;
align-items: center;
justify-content: center;
position: relative;
.label {
position: absolute;
top: 0;
width: 100%;
overflow: hidden;
text-align: center;
}
.thumb {
width: auto;
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 }) {
const { state, actions } = useAudioAsset(topicId, asset);
const { label } = asset.encrypted || asset.audio;
return (
<div className={classes.asset}>
<Image className={classes.thumb} src={audio} fit="contain" />
<div className={classes.label}>{ label }</div>
</div>
)
}

View File

@ -1,8 +1,27 @@
.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 {
width: auto;
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 }) {
const { state, actions } = useBinaryAsset(topicId, asset);
const { label, extension } = asset.encrypted || asset.binary;
return (
<div className={classes.asset}>
<Image className={classes.thumb} src={binary} fit="contain" />
<div className={classes.label}>
<div>{ label }</div>
<div className={classes.extension}>{ extension }</div>
</div>
</div>
)
}

View File

@ -135,8 +135,8 @@ export interface Focus {
setBlockTopic(topicId: string): Promise<void>;
clearBlockTopic(topicId: string): Promise<void>;
addTopicListener(ev: (topics: Topic[]) => void): void;
removeTopicListener(ev: (topics: Topic[]) => void): void;
addTopicListener(ev: (topics: null | Topic[]) => void): void;
removeTopicListener(ev: (topics: null | Topic[]) => 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;

View File

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

View File

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