mirror of
https://github.com/balzack/databag.git
synced 2025-04-23 18:15:19 +00:00
rendering message
This commit is contained in:
parent
2e170f693f
commit
c53bfeb039
@ -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;
|
||||
|
@ -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'}}/>
|
||||
|
@ -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 |
@ -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;
|
||||
|
@ -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' && (
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user