more webapp conversation refactor

This commit is contained in:
Roland Osborne 2023-01-25 23:26:40 -08:00
parent ba279dfcda
commit 3ceea69f99
20 changed files with 139 additions and 234 deletions

View File

@ -5,6 +5,7 @@ import { useAdmin } from './useAdmin.hook';
export function Admin() {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useAdmin();
const login = async () => {
@ -12,9 +13,10 @@ export function Admin() {
await actions.login();
}
catch(err) {
Modal.error({
modal.error({
title: 'Login Error',
content: 'Please confirm your password.',
bodyStyle: { padding: 16 },
});
}
}
@ -27,6 +29,7 @@ export function Admin() {
return (
<AdminWrapper>
{ modalContext }
<div className="app-title">
<span>Databag</span>
<div className="settings" onClick={() => actions.navUser()}>

View File

@ -5,6 +5,7 @@ import { useCreateAccount } from './useCreateAccount.hook';
export function CreateAccount() {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useCreateAccount();
const create = async () => {
@ -12,9 +13,10 @@ export function CreateAccount() {
await actions.onCreateAccount();
}
catch(err) {
Modal.error({
modal.error({
title: 'Create Account Error',
content: 'Please check with you administrator.',
bodyStyle: { padding: 16 },
});
}
}
@ -27,6 +29,7 @@ export function CreateAccount() {
return (
<CreateAccountWrapper>
{ modalContext }
<div className="app-title">
<span>Databag</span>
<div className="settings" onClick={() => actions.onSettings()}>

View File

@ -5,6 +5,7 @@ import { useLogin } from './useLogin.hook';
export function Login() {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useLogin();
const login = async () => {
@ -12,9 +13,10 @@ export function Login() {
await actions.onLogin();
}
catch(err) {
Modal.error({
modal.error({
title: 'Login Error',
content: 'Please confirm your username and password.',
bodyStyle: { padding: 16 },
});
}
}
@ -27,6 +29,7 @@ export function Login() {
return (
<LoginWrapper>
{ modalContext }
<div className="app-title">
<span>Databag</span>
<div className="settings" onClick={() => actions.onSettings()}>

View File

@ -8,6 +8,7 @@ export function useConversationContext() {
const [state, setState] = useState({
offsync: false,
topics: new Map(),
card: null,
channel: null,
topicRevision: null,
});
@ -92,7 +93,7 @@ export function useConversationContext() {
else {
await channel.actions.addTopic(channelId, type, message, files);
}
await resync();
resync();
};
const removeTopic = async (cardId, channelId, topicId) => {
@ -115,7 +116,7 @@ export function useConversationContext() {
await resync();
};
const getTopicAssetUrl = async (cardId, channelId, topicId, assetId) => {
const getTopicAssetUrl = (cardId, channelId, topicId, assetId) => {
if (cardId) {
return card.actions.getTopicAssetUrl(cardId, channelId, topicId, assetId);
}
@ -183,8 +184,9 @@ export function useConversationContext() {
// sync channel details
if (setDetailRevision.current !== detailRevision) {
let channelSync;
let cardSync;
if (cardId) {
const cardSync = card.state.cards.get(cardId);
cardSync = card.state.cards.get(cardId);
channelSync = cardSync?.channels.get(channelId);
}
else {
@ -192,7 +194,7 @@ export function useConversationContext() {
}
if (channelSync) {
setDetailRevision.current = detailRevision;
updateState({ channel: channelSync });
updateState({ card: cardSync, channel: channelSync });
}
else {
syncing.current = false;
@ -310,7 +312,7 @@ export function useConversationContext() {
const { cardId, channelId } = conversationId.current;
await setTopicSubject(cardId, channelId, type, subject);
},
getTopicAssetUrl: (topicId, assetId) => {
getTopicAssetUrl: (assetId, topicId) => {
const { cardId, channelId } = conversationId.current;
return getTopicAssetUrl(cardId, channelId, topicId, assetId);
},

View File

@ -101,7 +101,7 @@ export function Dashboard() {
</div>
</div>
<Modal title="Settings" visible={state.showSettings} centered
<Modal title="Settings" visible={state.showSettings} centered bodyStyle={{ padding: 16 }}
okText="Save" onOk={() => actions.setSettings()} onCancel={() => actions.setShowSettings(false)}>
<SettingsLayout direction="vertical">
<div className="field">
@ -144,7 +144,7 @@ export function Dashboard() {
</div>
</SettingsLayout>
</Modal>
<Modal title="Create Account" visible={state.showCreate} centered width="fitContent"
<Modal bodyStyle={{ padding: 16 }} title="Create Account" visible={state.showCreate} centered width="fitContent"
footer={[ <Button type="primary" onClick={() => actions.setShowCreate(false)}>OK</Button> ]}
onCancel={() => actions.setShowCreate(false)}>
<CreateLayout>

View File

@ -6,12 +6,14 @@ import { Modal, Tooltip, Button } from 'antd';
export function AccountItem({ item, remove }) {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useAccountItem(item, remove);
const removeAccount = () => {
Modal.confirm({
modal.confirm({
title: 'Are you sure you want to delete the account?',
icon: <ExclamationCircleOutlined />,
bodyStyle: { padding: 16 },
onOk() {
applyRemoveAccount();
},
@ -24,9 +26,10 @@ export function AccountItem({ item, remove }) {
await actions.remove();
}
catch(err) {
Modal.error({
modal.error({
title: 'Failed to Remove Account',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -36,9 +39,10 @@ export function AccountItem({ item, remove }) {
await actions.setStatus(status);
}
catch(err) {
Modal.error({
modal.error({
title: 'Failed to Set Account Status',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -48,9 +52,10 @@ export function AccountItem({ item, remove }) {
await actions.setAccessLink();
}
catch(err) {
Modal.error({
modal.error({
title: 'Failed to Set Account Access',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -65,6 +70,7 @@ export function AccountItem({ item, remove }) {
return (
<AccountItemWrapper>
{ modalContext }
<div className="avatar">
<Logo url={state.imageUrl} width={32} height={32} radius={4} />
</div>
@ -116,7 +122,7 @@ export function AccountItem({ item, remove }) {
</div>
<Modal title="Access Account" visible={state.showAccess} centered width="fitContent"
footer={[ <Button type="primary" onClick={() => actions.setShowAccess(false)}>OK</Button> ]}
onCancel={() => actions.setShowAccess(false)}>
bodyStyle={{ padding: 16 }} onCancel={() => actions.setShowAccess(false)}>
<AccessLayout>
<div className="url">
<div className="label">Browser Link:</div>

View File

@ -1,3 +1,17 @@
.ant-modal-content {
padding: 0 !important;
}
.ant-modal-title {
padding-top: 16px !important;
padding-left: 16px !important;
}
.ant-modal-footer {
padding-right: 16px !important;
padding-bottom: 16px !important;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@ -31,6 +31,7 @@ export function Profile({ closeProfile }) {
modal.error({
title: 'Failed to Save',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -45,6 +46,7 @@ export function Profile({ closeProfile }) {
modal.error({
title: 'Failed to Save',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -53,6 +55,7 @@ export function Profile({ closeProfile }) {
modal.confirm({
title: 'Are you sure you want to logout?',
icon: <LogoutOutlined />,
bodyStyle: { padding: 16 },
onOk() {
actions.logout();
},
@ -155,7 +158,7 @@ export function Profile({ closeProfile }) {
</div>
)}
<Modal title="Profile Image" centered visible={state.editProfileImage} footer={editImageFooter}
onCancel={actions.clearEditProfileImage}>
style={{ padding: 16 }} onCancel={actions.clearEditProfileImage}>
<ProfileImageWrapper>
<Cropper image={state.editImage} crop={state.crop} zoom={state.zoom} aspect={1}
@ -164,7 +167,7 @@ export function Profile({ closeProfile }) {
</Modal>
<Modal title="Profile Details" centered visible={state.editProfileDetails} footer={editDetailsFooter}
onCancel={actions.clearEditProfileDetails}>
style={{ padding: 16 }} onCancel={actions.clearEditProfileDetails}>
<ProfileDetailsWrapper>
<div class="info">

View File

@ -18,6 +18,7 @@ export function AccountAccess() {
modal.error({
title: 'Failed to Set Sealing Key',
comment: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -31,6 +32,7 @@ export function AccountAccess() {
modal.error({
title: 'Update Registry Failed',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
};
@ -45,6 +47,7 @@ export function AccountAccess() {
modal.error({
title: 'Failed to Save',
comment: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}

View File

@ -45,7 +45,7 @@ export function Channels({ open, active }) {
<Button type="primary" icon={<CommentOutlined />} onClick={actions.setShowAdd}>New Topic</Button>
</div>
)}
<Modal title="New Topic" centered visible={state.showAdd} footer={null} destroyOnClose={true}
<Modal bodyStyle={{ padding: 16 }} title="New Topic" centered visible={state.showAdd} footer={null} destroyOnClose={true}
onCancel={actions.clearShowAdd}>
<AddChannel added={added} cancelled={actions.clearShowAdd} />
</Modal>

View File

@ -18,6 +18,7 @@ export function AddChannel({ added, cancelled }) {
modal.error({
title: 'Failed to Create Topic',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}

View File

@ -7,6 +7,7 @@ import { Logo } from 'logo/Logo';
import { AddTopic } from './addTopic/AddTopic';
import { TopicItem } from './topicItem/TopicItem';
import { List, Spin, Tooltip } from 'antd';
import { ChannelHeader } from './channelHeader/ChannelHeader';
export function Conversation({ closeConversation, openDetails, cardId, channelId }) {
@ -38,36 +39,7 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
return (
<ConversationWrapper>
<div class="header">
<div class="title">
<div class="logo">
<Logo img={state.logoImg} url={state.logoUrl} width={32} height={32} radius={4} />
</div>
<div class="label">{ state.subject }</div>
{ state.error && state.display === 'small' && (
<StatusError onClick={actions.resync}>
<ExclamationCircleOutlined />
</StatusError>
)}
{ state.error && state.display !== 'small' && (
<Tooltip placement="bottom" title="sync error">
<StatusError onClick={actions.resync}>
<ExclamationCircleOutlined />
</StatusError>
</Tooltip>
)}
{ state.display !== 'xlarge' && (
<div class="button" onClick={openDetails}>
<SettingOutlined />
</div>
)}
</div>
{ state.display !== 'xlarge' && (
<div class="button" onClick={closeConversation}>
<CloseOutlined />
</div>
)}
</div>
<ChannelHeader openDetails={openDetails} closeConversation={closeConversation} contentKey={state.contentKey}/>
<div class="thread" ref={thread} onScroll={scrollThread}>
{ state.delayed && state.topics.length === 0 && (
<div class="empty">This Topic Has No Messages</div>

View File

@ -1,5 +1,4 @@
import { TopicItemWrapper } from './TopicItem.styled';
import { useTopicItem } from './useTopicItem.hook';
import { VideoAsset } from './videoAsset/VideoAsset';
import { AudioAsset } from './audioAsset/AudioAsset';
import { ImageAsset } from './imageAsset/ImageAsset';
@ -10,6 +9,21 @@ import { Carousel } from 'carousel/Carousel';
export function TopicItem({ host, topic }) {
const renderAsset = (asset, idx) => {
if (asset.image) {
return <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
fullUrl={topic.assetUrl(asset.image.full, topic.id)} />
}
if (asset.video) {
return <VideoAsset thumbUrl={topic.assetUrl(asset.video.thumb, topic.id)}
lqUrl={topic.assetUrl(asset.video.lq, topic.id)} hdUrl={topic.assetUrl(asset.video.hd, topic.id)} />
}
if (asset.audio) {
return <AudioAsset label={asset.audio.label} audioUrl={topic.assetUrl(asset.audio.full, topic.id)} />
}
return <></>
}
return (
<TopicItemWrapper>
<div class="topic-header">
@ -21,9 +35,37 @@ export function TopicItem({ host, topic }) {
<div>{ topic.createdStr }</div>
</div>
</div>
<div class="message">
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
</div>
{ topic.status !== 'confirmed' && (
<div class="skeleton">
<Skeleton size={'small'} active={true} title={false} />
</div>
)}
{ topic.status === 'confirmed' && (
<>
{ topic.assets?.length && (
<>
{ topic.transform === 'error' && (
<div class="asset-placeholder">
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
</div>
)}
{ topic.transform === 'incomplete' && (
<div class="asset-placeholder">
<PictureOutlined style={{ fontSize: 32 }} />
</div>
)}
{ topic.transform === 'complete' && (
<div class="topic-assets">
<Carousel pad={40} items={topic.assets} itemRenderer={renderAsset} />
</div>
)}
</>
)}
<div class="message">
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
</div>
</>
)}
</TopicItemWrapper>
)
}

View File

@ -1,169 +0,0 @@
import { useContext, useState, useEffect, useRef } from 'react';
import { ConversationContext } from 'context/ConversationContext';
import { ProfileContext } from 'context/ProfileContext';
import { CardContext } from 'context/CardContext';
import { getProfileByGuid } from 'context/cardUtil';
export function useTopicItem(topic, sealed, sealKey) {
const [state, setState] = useState({
init: false,
name: null,
handle: null,
imageUrl: null,
text: null,
created: null,
confirmed: false,
ready: false,
error: false,
owner: false,
assets: [],
topicId: null,
editing: false,
busy: false,
textColor: '#444444',
textSize: 14,
});
const profile = useContext(ProfileContext);
const card = useContext(CardContext);
const conversation = useContext(ConversationContext);
const editMessage = useRef(null);
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
useEffect(() => {
let owner = false;
if (profile.state.identity.guid === topic?.data?.topicDetail.guid) {
owner = true;
}
let text = null;
let textColor = '#444444';
let textSize = 14;
if (!topic?.data) {
console.log("invalid topic:", topic);
return;
}
const { status, transform, data, dataType } = topic.data.topicDetail;
let message;
let sealed = false;
let ready = false;
let error = false;
let confirmed = false;
let assets = [];
if (status === 'confirmed') {
confirmed = true;
if (dataType === 'superbasictopic') {
try {
message = JSON.parse(data);
if (typeof message.text === 'string') {
text = message.text;
}
if (message.textColor != null) {
textColor = message.textColor;
}
if (message.textSize != null) {
textSize = message.textSize;
}
if (message.assets) {
assets = message.assets;
delete message.assets;
}
if (transform === 'complete') {
ready = true;
}
if (transform === 'error') {
error = true;
}
}
catch(err) {
console.log(err);
}
}
else if (dataType === 'sealedtopic') {
if (topic.data.unsealedMessage) {
text = topic.data.unsealedMessage.message?.text;
sealed = false;
}
else {
if (sealKey) {
conversation.actions.unsealTopic(topic.id, sealKey);
}
sealed = true;
}
ready = true;
}
}
if (profile.state.identity?.guid) {
const { guid, created } = topic.data.topicDetail;
let createdStr;
const date = new Date(created * 1000);
const now = new Date();
const offset = now.getTime() - date.getTime();
if(offset < 86400000) {
createdStr = date.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'});
}
else if (offset < 31449600000) {
createdStr = date.toLocaleDateString("en-US", {day: 'numeric', month:'numeric'});
}
else {
createdStr = date.toLocaleDateString("en-US");
}
if (profile.state.identity.guid === guid) {
const { name, handle } = profile.state.identity;
const imageUrl = profile.state.imageUrl;
updateState({ sealed, name, handle, imageUrl, status, text, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true });
}
else {
const { cardId, name, handle, imageSet } = getProfileByGuid(card.state.cards, guid);
const imageUrl = imageSet ? card.actions.getCardImageUrl(cardId) : null;
updateState({ sealed, name, handle, imageUrl, status, text, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true });
}
}
}, [profile, card, conversation, topic, sealKey]);
const actions = {
getAssetUrl: (assetId, topicId) => {
return conversation.actions.getAssetUrl(state.topicId, assetId);
},
removeTopic: async () => {
return await conversation.actions.removeTopic(topic.id);
},
setEditing: (editing) => {
editMessage.current = state.message?.text;
updateState({ editing });
},
setEdit: (edit) => {
editMessage.current = edit;
},
setMessage: async () => {
if (!state.busy) {
updateState({ busy: true });
try {
if (sealed) {
await conversation.actions.setSealedTopicSubject(topic.id, {...state.message, text: editMessage.current }, sealKey);
}
else {
await conversation.actions.setTopicSubject(topic.id,
{ ...state.message, text: editMessage.current, assets: state.assets });
}
updateState({ editing: false });
}
catch (err) {
window.alert(err);
}
updateState({ busy: false });
}
},
};
return { state, actions };
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Modal } from 'antd';
import ReactResizeDetector from 'react-resize-detector';
import { VideoCameraOutlined } from '@ant-design/icons';
@ -10,6 +10,10 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
const { state, actions } = useVideoAsset();
const [dimension, setDimension] = useState({ width: 0, height: 0 });
useEffect(() => {
console.log(dimension);
}, [dimension]);
const activate = () => {
if (dimension.width / dimension.height > window.innerWidth / window.innerHeight) {
let width = Math.floor(window.innerWidth * 8 / 10);
@ -39,7 +43,7 @@ export function VideoAsset({ thumbUrl, lqUrl, hdUrl }) {
<VideoCameraOutlined style={{ fontSize: 32, color: '#eeeeee', cursor: 'pointer' }} />
</div>
)}
<Modal centered={true} visible={state.active} width={state.width + 12} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd' }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
<Modal centered={true} style={{ backgroundColor: '#aacc00', padding: 0 }} visible={state.active} width={state.width + 12} bodyStyle={{ paddingBottom: 0, paddingTop: 6, paddingLeft: 6, paddingRight: 6, backgroundColor: '#dddddd', margin: 0 }} footer={null} destroyOnClose={true} closable={false} onCancel={actions.clearActive}>
<video autoplay="true" controls src={hdUrl} width={state.width} height={state.height}
playsinline="true" />
</Modal>

View File

@ -106,8 +106,8 @@ export function useConversation(cardId, channelId) {
const { card, channel } = conversationId.current;
loading.current = true;
conversationId.current = null;
updateState({ loading: true });
await conversation.setChannel(card, channel);
updateState({ loading: true, contentKey: null });
await conversation.actions.setChannel(card, channel);
updateState({ loading: false });
loading.current = false;
await setChannel();
@ -115,7 +115,8 @@ export function useConversation(cardId, channelId) {
}
useEffect(() => {
conversation.actions.setChannel(cardId, channelId);
conversationId.current = { card: cardId, channel: channelId };
setChannel();
// eslint-disable-next-line
}, [cardId, channelId]);
@ -175,6 +176,7 @@ export function useConversation(cardId, channelId) {
if (item.revision !== revision) {
try {
const message = JSON.parse(detail.data);
item.assets = message.assets;
item.text = message.text;
item.textColor = message.textColor ? message.textColor : '#444444';
item.textSize = message.textSize ? message.textSize : 14;
@ -189,6 +191,7 @@ export function useConversation(cardId, channelId) {
item.contentKey = state.contentKey;
try {
const subject = decryptTopicSubject(detail.data, state.contentKey);
item.assets = subject.message.assets;
item.text = subject.message.text;
item.textColor = subject.message.textColor ? subject.message.textColor : '#444444';
item.textSize = subject.message.textSize ? subject.message.textSize : 14;
@ -198,18 +201,21 @@ export function useConversation(cardId, channelId) {
}
}
}
item.transform = detail.transform;
item.status = detail.status;
item.assetUrl = conversation.actions.getTopicAssetUrl;
item.revision = revision;
};
useEffect(() => {
const messages = new Map();
conversation.state.topics.forEach((value, topicId) => {
let item = topics.current.get(topicId);
conversation.state.topics.forEach((value, id) => {
let item = topics.current.get(id);
if (!item) {
item = { topicId };
item = { id };
}
syncTopic(item, value);
messages.set(topicId, item);
messages.set(id, item);
});
topics.current = messages;

View File

@ -18,6 +18,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
modal.confirm({
title: 'Are you sure you want to delete the topic?',
icon: <ExclamationCircleOutlined />,
bodyStyle: { padding: 16 },
okText: "Yes, Delete",
cancelText: "No, Cancel",
onOk() {
@ -36,6 +37,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
modal.error({
title: 'Failed to Delete Topic',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -44,6 +46,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
modal.confirm({
title: 'Are you sure you want to leave the topic?',
icon: <ExclamationCircleOutlined />,
bodyStyle: { padding: 16 },
okText: "Yes, Leave",
cancelText: "No, Cancel",
onOk() {
@ -62,6 +65,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
modal.error({
title: 'Failed to Leave Topic',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
}
@ -75,6 +79,7 @@ export function Details({ cardId, channelId, closeDetails, closeConversation, op
modal.error({
title: 'Failed to Update Subject',
content: 'Please try again.',
bodyStyle: { padding: 16 },
});
}
};

View File

@ -88,10 +88,15 @@ export function useDetails(cardId, channelId) {
else {
locked = false;
editable = (chan.cardId == null);
const parsed = JSON.parse(chan.data.channelDetail.data);
if (parsed.subject) {
subject = parsed.subject;
subjectUpdate = subject;
try {
const parsed = JSON.parse(chan.data.channelDetail.data);
if (parsed.subject) {
subject = parsed.subject;
subjectUpdate = subject;
}
}
catch(err) {
console.log(err);
}
}
const date = new Date(chan.data.channelDetail.created * 1000);

View File

@ -13,6 +13,7 @@ export function Identity({ openAccount, openCards, cardUpdated }) {
modal.confirm({
title: 'Are you sure you want to logout?',
icon: <LogoutOutlined />,
bodyStyle: { padding: 16 },
onOk() {
actions.logout();
},

View File

@ -18,6 +18,7 @@ export function Listing({ closeListing, openContact }) {
modal.error({
title: 'Communication Error',
content: 'Please confirm your server name.',
bodyStyle: { padding: 16 },
});
}
}