added edit subject

This commit is contained in:
balzack 2024-12-19 22:13:57 -08:00
parent 01c8c2773c
commit 81a58c3622
5 changed files with 120 additions and 70 deletions

View File

@ -36,7 +36,7 @@ export function Conversation() {
const attachAudio = useRef({ click: ()=>{} } as HTMLInputElement);
const attachBinary = useRef({ click: ()=>{} } as HTMLInputElement);
const { width, height, ref } = useResizeDetector();
const input = useRef();
const input = useRef(null as null | HTMLTextAreaElement);
const addImage = (image: File | undefined) => {
if (image) {
@ -118,8 +118,10 @@ export function Conversation() {
}
}
useEffect(() => {
input.current.focus();
useEffect(() => {
if (input.current) {
input.current.focus();
}
}, [sending]);
const topics = state.topics.map((topic, idx) => {

View File

@ -42,6 +42,17 @@
display: none;
}
.editing {
margin-top: 12px;
}
.controls {
padding: 4px;
justify-content: flex-end;
display: flex;
gap: 8px;
}
.thumbs {
display: flex;
flex-direction: row;

View File

@ -2,14 +2,14 @@ import { useRef, useEffect, 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, ActionIcon } from '@mantine/core'
import { Textarea, Button, Image, Skeleton, ActionIcon } from '@mantine/core'
import { ImageAsset } from './imageAsset/ImageAsset';
import { AudioAsset } from './audioAsset/AudioAsset';
import { VideoAsset } from './videoAsset/VideoAsset';
import { BinaryAsset } from './binaryAsset/BinaryAsset';
import type { MediaAsset } from '../conversation/Conversation';
import { useMessage } from './useMessage.hook';
import { IconForbid, IconTrash, IconShare, IconEdit, IconAlertSquareRounded, IconChevronLeft, IconChevronRight, IconFileAlert } from '@tabler/icons-react';
import { IconForbid, IconTrash, IconEdit, IconAlertSquareRounded, IconChevronLeft, IconChevronRight, IconFileAlert } from '@tabler/icons-react';
import { useResizeDetector } from 'react-resize-detector';
import DOMPurify from 'dompurify';
@ -19,11 +19,30 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
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 }
const textStyle = textColor && textSize ? { color: textColor, fontSize: textSize } : textColor ? { color: textColor } : textSize ? { fontSize: textSize } : {}
const textStyle = { color: textColor ? textColor : undefined, fontSize: textSize ? textSize : undefined };
const logoUrl = profile ? profile.imageUrl : card ? card.imageUrl : avatar;
const timestamp = actions.getTimestamp(created);
const [message, setMessage] = useState(<p></p>);
const [editing, setEditing] = useState(false);
const [editText, setEditText] = useState('');
const [saving, setSaving] = useState(false);
const save = async () => {
setSaving(true);
try {
await actions.saveSubject(topic.topicId, topic.sealed, {...topic.data, text: editText});
} catch (err) {
console.log(err);
}
setSaving(false);
setEditing(false);
}
const edit = () => {
setEditing(true);
setEditText(text);
}
useEffect(() => {
const urlPattern = new RegExp('(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)');
const hostPattern = new RegExp('^https?:\\/\\/', 'i');
@ -105,9 +124,8 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
</div>
<div className={classes.options}>
<div className={classes.surface}>
<IconShare className={classes.option} />
{ !locked && profile && (
<IconEdit className={classes.option} />
<IconEdit className={classes.option} onClick={edit} />
)}
{ (host || profile) && (
<IconTrash className={classes.careful} />
@ -117,13 +135,24 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
</div>
</div>
</div>
{ !locked && status === 'confirmed' && text && (
<div style={textStyle}>
<div className={classes.padding}>
<span className={classes.text}>{ message }</span>
{! locked && status === 'confirmed' && editing && (
<div className={classes.editing}>
<Textarea styles={{ input: textStyle }} value={editText} onChange={(event) => setEditText(event.currentTarget.value)} placeholder={state.strings.newMessage} />
<div className={classes.controls}>
<Button variant="default" size="xs" onClick={() => setEditing(false)}>
{state.strings.cancel}
</Button>
<Button variant="filled" size="xs" onClick={save} loading={saving}>
{state.strings.save}
</Button>
</div>
</div>
)}
{ !locked && status === 'confirmed' && text && !editing && (
<div className={classes.padding} style={textStyle}>
<span className={classes.text}>{ message }</span>
</div>
)}
{ !locked && status !== 'confirmed' && (
<div className={classes.unconfirmed}>
<Skeleton height={8} mt={6} radius="xl" />

View File

@ -1,9 +1,10 @@
import { useState, useContext, useEffect } from 'react'
import { DisplayContext } from '../context/DisplayContext'
import { AppContext } from '../context/AppContext';
import { ContextType } from '../context/ContextType'
export function useMessage() {
const app = useContext(AppContext) as ContextType
const display = useContext(DisplayContext) as ContextType
const [state, setState] = useState({
strings: display.state.strings,
@ -22,6 +23,13 @@ export function useMessage() {
}, [display.state]);
const actions = {
saveSubject: async (topicId: string, sealed: boolean, subject: any) => {
const focus = app.state.focus;
if (focus) {
console.log("SAVING", subject);
await focus.setTopicSubject(topicId, sealed ? 'sealedtopic' : 'superbasictopic', ()=>subject, [], ()=>true);
}
},
getTimestamp: (created: number) => {
const now = Math.floor((new Date()).getTime() / 1000)
const date = new Date(created * 1000);

View File

@ -694,68 +694,68 @@ export class FocusModule implements Focus {
}
}
}
const { text, textColor, textSize, assets } = subject(appAsset);
}
const { text, textColor, textSize, assets } = subject(appAsset);
// legacy support of 'superbasictopic' and 'sealedtopic'
const getAsset = (assetId: string) => {
const index = parseInt(assetId);
const item = assetItems[index];
if (!item) {
throw new Error('invalid assetId in subject');
}
if (item.hosting === HostingMode.Inline) {
return item.inline;
} if (item.hosting === HostingMode.Split) {
return item.split;
} if (item.hosting === HostingMode.Basic) {
return item.basic;
} else {
throw new Error('unknown hosting mode');
}
// legacy support of 'superbasictopic' and 'sealedtopic'
const getAsset = (assetId: string) => {
const index = parseInt(assetId);
const item = assetItems[index];
if (!item) {
throw new Error('invalid assetId in subject');
}
const filtered = !assets ? [] : assets.filter((asset: any) => {
if (sealed && asset.encrypted) {
return true;
} else if (!sealed && !asset.encrypted) {
return true;
} else {
return false;
}
});
const mapped = filtered.map((asset: any) => {
if (sealed) {
const { type, thumb, parts } = asset.encrypted;
return { encrypted: { type, thumb: getAsset(thumb), parts: getAsset(parts) } };
} else if (asset.image) {
const { thumb, full } = asset.image;
return { image: { thumb: getAsset(thumb), full: getAsset(full) } };
} else if (asset.video) {
const { thumb, lq, hd } = asset.video;
return { video: { thumb: getAsset(thumb), lq: getAsset(lq), hd: getAsset(hd) } };
} else if (asset.audio) {
const { label, full } = asset.audio;
return { audio: { label, full: getAsset(full) } };
} else if (asset.binary) {
const { label, extension, data } = asset.binary;
return { binary: { label, extension, data: getAsset(data) } };
}
});
const updated = { text, textColor, textSize, assets: mapped };
// end of legacy support block
if (sealed) {
if (!crypto || !channelKey) {
throw new Error('encryption not set');
}
const subjectString = JSON.stringify({ message: updated });
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(subjectString, ivHex, channelKey);
const data = { messageEncrypted: encryptedDataB64, messageIv: ivHex };
return await this.setRemoteChannelTopicSubject(topicId, type, data);
if (item.hosting === HostingMode.Inline) {
return item.inline;
} if (item.hosting === HostingMode.Split) {
return item.split;
} if (item.hosting === HostingMode.Basic) {
return item.basic;
} else {
return await this.setRemoteChannelTopicSubject(topicId, type, updated);
throw new Error('unknown hosting mode');
}
}
const filtered = !assets ? [] : assets.filter((asset: any) => {
if (sealed && asset.encrypted) {
return true;
} else if (!sealed && !asset.encrypted) {
return true;
} else {
return false;
}
});
const mapped = filtered.map((asset: any) => {
if (sealed) {
const { type, thumb, parts } = asset.encrypted;
return { encrypted: { type, thumb: getAsset(thumb), parts: getAsset(parts) } };
} else if (asset.image) {
const { thumb, full } = asset.image;
return { image: { thumb: getAsset(thumb), full: getAsset(full) } };
} else if (asset.video) {
const { thumb, lq, hd } = asset.video;
return { video: { thumb: getAsset(thumb), lq: getAsset(lq), hd: getAsset(hd) } };
} else if (asset.audio) {
const { label, full } = asset.audio;
return { audio: { label, full: getAsset(full) } };
} else if (asset.binary) {
const { label, extension, data } = asset.binary;
return { binary: { label, extension, data: getAsset(data) } };
}
});
const updated = { text, textColor, textSize, assets: mapped };
// end of legacy support block
if (sealed) {
if (!crypto || !channelKey) {
throw new Error('encryption not set');
}
const subjectString = JSON.stringify({ message: updated });
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(subjectString, ivHex, channelKey);
const data = { messageEncrypted: encryptedDataB64, messageIv: ivHex };
return await this.setRemoteChannelTopicSubject(topicId, type, data);
} else {
return await this.setRemoteChannelTopicSubject(topicId, type, updated);
}
}
public async removeTopic(topicId: string) {