mirror of
https://github.com/balzack/databag.git
synced 2025-02-14 12:39:17 +00:00
more conversation refactor
This commit is contained in:
parent
86d0e31fa7
commit
1875ca759d
@ -106,12 +106,12 @@ export function useConversationContext() {
|
||||
await resync();
|
||||
};
|
||||
|
||||
const setTopicSubject = async (cardId, channelId, type, subject) => {
|
||||
const setTopicSubject = async (cardId, channelId, topicId, type, subject) => {
|
||||
if (cardId) {
|
||||
await card.actions.setTopicSubject(cardId, channelId, type, subject);
|
||||
await card.actions.setTopicSubject(cardId, channelId, topicId, type, subject);
|
||||
}
|
||||
else {
|
||||
await channel.actions.setTopicSubject(channelId, type, subject);
|
||||
await channel.actions.setTopicSubject(channelId, topicId, type, subject);
|
||||
}
|
||||
await resync();
|
||||
};
|
||||
@ -308,9 +308,9 @@ export function useConversationContext() {
|
||||
const { cardId, channelId } = conversationId.current;
|
||||
await removeTopic(cardId, channelId, topicId);
|
||||
},
|
||||
setTopicSubject: async (type, subject) => {
|
||||
setTopicSubject: async (topicId, type, subject) => {
|
||||
const { cardId, channelId } = conversationId.current;
|
||||
await setTopicSubject(cardId, channelId, type, subject);
|
||||
await setTopicSubject(cardId, channelId, topicId, type, subject);
|
||||
},
|
||||
getTopicAssetUrl: (assetId, topicId) => {
|
||||
const { cardId, channelId } = conversationId.current;
|
||||
|
@ -91,7 +91,7 @@ export function AccountAccess() {
|
||||
<LockOutlined />
|
||||
<div className="label">Change Login</div>
|
||||
</div>
|
||||
<Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal}>
|
||||
<Modal title="Topic Sealing Key" centered visible={state.editSeal} footer={editSealFooter} onCancel={actions.clearEditSeal} bodyStyle={{ padding: 16 }}>
|
||||
<SealModal>
|
||||
<div className="switch">
|
||||
<Switch size="small" checked={state.sealEnabled} onChange={enable => actions.enableSeal(enable)} />
|
||||
@ -130,7 +130,7 @@ export function AccountAccess() {
|
||||
</SealModal>
|
||||
</Modal>
|
||||
<Modal title="Account Login" centered visible={state.editLogin} footer={editLoginFooter}
|
||||
onCancel={actions.clearEditLogin}>
|
||||
bodyStyle={{ paddingLeft: 16, paddingRight: 16 }} onCancel={actions.clearEditLogin}>
|
||||
<Form name="basic" wrapperCol={{ span: 24, }}>
|
||||
<Form.Item name="username" validateStatus={state.editStatus} help={state.editMessage}>
|
||||
<Input placeholder="Username" spellCheck="false" onChange={(e) => actions.setEditHandle(e.target.value)}
|
||||
|
@ -15,7 +15,11 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
|
||||
const thread = useRef(null);
|
||||
|
||||
const topicRenderer = (topic) => {
|
||||
return (<TopicItem host={cardId == null} topic={topic} remove={() => actions.removeTopic(topic.id)}/>)
|
||||
return (<TopicItem host={cardId == null} topic={topic}
|
||||
remove={() => actions.removeTopic(topic.id)}
|
||||
update={(text) => actions.updateTopic(topic, text)}
|
||||
sealed={state.sealed && !state.contentKey}
|
||||
/>)
|
||||
}
|
||||
|
||||
// an unfortunate cludge for the mobile browser
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { useChannelHeader } from './useChannelHeader.hook';
|
||||
import { ChannelHeaderWrapper, StatusError } from './ChannelHeader.styled';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Logo } from 'logo/Logo';
|
||||
|
||||
export function ChannelHeader({ closeConversation, openDetails, contentKey }) {
|
||||
|
||||
const { state, actions } = useChannelHeader(contentKey);
|
||||
|
||||
return (
|
||||
<ChannelHeaderWrapper>
|
||||
<div class="title">
|
||||
<div class="logo">
|
||||
<Logo img={state.img} url={state.logo} width={32} height={32} radius={4} />
|
||||
</div>
|
||||
{ state.title && (
|
||||
<div class="label">{ state.title }</div>
|
||||
)}
|
||||
{ !state.title && (
|
||||
<div class="label">{ state.label }</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>
|
||||
)}
|
||||
</ChannelHeaderWrapper>
|
||||
);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import styled from 'styled-components';
|
||||
import Colors from 'constants/Colors';
|
||||
|
||||
export const ChannelHeaderWrapper = styled.div`
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
height: 48px;
|
||||
border-bottom: 1px solid ${Colors.profileDivider};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
flex-grow: 1;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
.label {
|
||||
padding-left: 8px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 18px;
|
||||
color: ${Colors.grey};
|
||||
cursor: pointer;
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
export const StatusError = styled.div`
|
||||
color: ${Colors.error};
|
||||
font-size: 14px;
|
||||
padding-left: 8px;
|
||||
cursor: pointer;
|
||||
`
|
@ -0,0 +1,131 @@
|
||||
import { useState, useContext, useEffect, useRef } from 'react';
|
||||
import { ViewportContext } from 'context/ViewportContext';
|
||||
import { ConversationContext } from 'context/ConversationContext';
|
||||
import { CardContext } from 'context/CardContext';
|
||||
import { ProfileContext } from 'context/ProfileContext';
|
||||
import { getCardByGuid } from 'context/cardUtil';
|
||||
import { decryptChannelSubject } from 'context/sealUtil';
|
||||
|
||||
export function useChannelHeader(contentKey) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
logoImg: null,
|
||||
logoUrl: null,
|
||||
label: null,
|
||||
title: null,
|
||||
offsync: false,
|
||||
display: null,
|
||||
});
|
||||
|
||||
const viewport = useContext(ViewportContext);
|
||||
const card = useContext(CardContext);
|
||||
const conversation = useContext(ConversationContext);
|
||||
const profile = useContext(ProfileContext);
|
||||
|
||||
const cardId = useRef();
|
||||
const channelId = useRef();
|
||||
const detailRevision = useRef();
|
||||
const key = useRef();
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateState({ display: viewport.state.display });
|
||||
}, [viewport.state]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const cardValue = conversation.state.card;
|
||||
const channelValue = conversation.state.channel;
|
||||
|
||||
// extract member info
|
||||
let memberCount = 0;
|
||||
let names = [];
|
||||
let img = null;
|
||||
let logo = null;
|
||||
if (cardValue) {
|
||||
const profile = cardValue.data?.cardProfile;
|
||||
if (profile?.name) {
|
||||
names.push(profile.name);
|
||||
}
|
||||
if (profile?.imageSet) {
|
||||
img = null;
|
||||
logo = card.actions.getCardImageUrl(cardValue.id);
|
||||
}
|
||||
else {
|
||||
img = 'avatar';
|
||||
logo = null;
|
||||
}
|
||||
memberCount++;
|
||||
}
|
||||
if (channelValue?.data?.channelDetail?.members) {
|
||||
for (let guid of channelValue.data.channelDetail.members) {
|
||||
if (guid !== profile.state.identity.guid) {
|
||||
const contact = getCardByGuid(card.state.cards, guid);
|
||||
const profile = contact?.data?.cardProfile;
|
||||
if (profile?.name) {
|
||||
names.push(profile.name);
|
||||
}
|
||||
if (profile?.imageSet) {
|
||||
img = null;
|
||||
logo = card.actions.getCardImageUrl(contact.id);
|
||||
}
|
||||
else {
|
||||
img = 'avatar';
|
||||
logo = null;
|
||||
}
|
||||
memberCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let label;
|
||||
if (memberCount === 0) {
|
||||
img = 'solution';
|
||||
label = 'Notes';
|
||||
}
|
||||
else if (memberCount === 1) {
|
||||
label = names.join(',');
|
||||
}
|
||||
else {
|
||||
img = 'appstore';
|
||||
label = names.join(',');
|
||||
}
|
||||
|
||||
if (cardId.current !== cardValue?.id || channelId.current !== channelValue?.id ||
|
||||
detailRevision.current !== channelValue?.data?.detailRevision || key.current !== contentKey) {
|
||||
let title;
|
||||
try {
|
||||
const detail = channelValue?.data?.channelDetail;
|
||||
if (detail?.dataType === 'sealed' && contentKey) {
|
||||
const unsealed = decryptChannelSubject(detail.data, contentKey);
|
||||
title = unsealed.subject;
|
||||
}
|
||||
else if (detail?.dataType === 'superbasic') {
|
||||
const data = JSON.parse(detail.data);
|
||||
title = data.subject;
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
}
|
||||
cardId.current = cardValue?.id;
|
||||
channelId.current = channelValue?.id;
|
||||
detailRevision.current = channelValue?.data?.detailRevision;
|
||||
key.current = contentKey;
|
||||
updateState({ title, label, img, logo });
|
||||
}
|
||||
else {
|
||||
updateState({ label, img, logo });
|
||||
}
|
||||
}, [conversation.state, card.state, contentKey]);
|
||||
|
||||
const actions = {
|
||||
resync: () => {
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
@ -6,10 +6,12 @@ import { Logo } from 'logo/Logo';
|
||||
import { Space, Skeleton, Button, Modal, Input } from 'antd';
|
||||
import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined, PictureOutlined } from '@ant-design/icons';
|
||||
import { Carousel } from 'carousel/Carousel';
|
||||
import { useTopicItem } from './useTopicItem.hook';
|
||||
|
||||
export function TopicItem({ host, topic, remove }) {
|
||||
export function TopicItem({ host, sealed, topic, update, remove }) {
|
||||
|
||||
const [ modal, modalContext ] = Modal.useModal();
|
||||
const { state, actions } = useTopicItem();
|
||||
|
||||
const removeTopic = () => {
|
||||
modal.confirm({
|
||||
@ -34,6 +36,21 @@ export function TopicItem({ host, topic, remove }) {
|
||||
});
|
||||
}
|
||||
|
||||
const updateTopic = async () => {
|
||||
try {
|
||||
await update(state.message);
|
||||
actions.clearEditing();
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
modal.error({
|
||||
title: 'Failed to Update Message',
|
||||
content: 'Please try again.',
|
||||
bodyStyle: { padding: 16 },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderAsset = (asset, idx) => {
|
||||
if (asset.image) {
|
||||
return <ImageAsset thumbUrl={topic.assetUrl(asset.image.thumb, topic.id)}
|
||||
@ -62,8 +79,8 @@ export function TopicItem({ host, topic, remove }) {
|
||||
</div>
|
||||
<div class="topic-options">
|
||||
<div class="buttons">
|
||||
{ topic.creator && (
|
||||
<div class="button edit" onClick={() => console.log('edit')}>
|
||||
{ !sealed && topic.creator && (
|
||||
<div class="button edit" onClick={() => actions.setEditing(topic.text)}>
|
||||
<EditOutlined />
|
||||
</div>
|
||||
)}
|
||||
@ -101,9 +118,27 @@ export function TopicItem({ host, topic, remove }) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div class="message">
|
||||
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
|
||||
</div>
|
||||
{ sealed && (
|
||||
<div class="sealed-message">sealed message</div>
|
||||
)}
|
||||
{ !sealed && !state.editing && (
|
||||
<div class="message">
|
||||
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.text }</div>
|
||||
</div>
|
||||
)}
|
||||
{ state.editing && (
|
||||
<div class="editing">
|
||||
<Input.TextArea defaultValue={state.message} placeholder="message"
|
||||
style={{ resize: 'none', color: state.textColor, fontSize: state.textSize }}
|
||||
onChange={(e) => actions.setMessage(e.target.value)} rows={3} bordered={false}/>
|
||||
<div class="controls">
|
||||
<Space>
|
||||
<Button onClick={actions.clearEditing}>Cancel</Button>
|
||||
<Button type="primary" onClick={updateTopic}>Save</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TopicItemWrapper>
|
||||
|
@ -88,6 +88,7 @@ export const TopicItemWrapper = styled.div`
|
||||
.sealed-message {
|
||||
font-style: italic;
|
||||
color: #aaaaaa;
|
||||
padding-left: 72px;
|
||||
}
|
||||
|
||||
.asset-placeholder {
|
||||
@ -103,6 +104,7 @@ export const TopicItemWrapper = styled.div`
|
||||
|
||||
.topic-assets {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
@ -116,23 +118,24 @@ export const TopicItemWrapper = styled.div`
|
||||
padding-left: 72px;
|
||||
white-space: pre-line;
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.editing {
|
||||
.editing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #aaaaaa;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 16px;
|
||||
margin-left: 72px;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #aaaaaa;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useTopicItem() {
|
||||
|
||||
const [state, setState] = useState({
|
||||
editing: false,
|
||||
message: null,
|
||||
});
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
}
|
||||
|
||||
const actions = {
|
||||
setEditing: (message) => {
|
||||
updateState({ editing: true, message });
|
||||
},
|
||||
clearEditing: () => {
|
||||
updateState({ editing: false });
|
||||
},
|
||||
setMessage: (message) => {
|
||||
updateState({ message });
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { UploadContext } from 'context/UploadContext';
|
||||
import { StoreContext } from 'context/StoreContext';
|
||||
import { CardContext } from 'context/CardContext';
|
||||
import { ProfileContext } from 'context/ProfileContext';
|
||||
import { isUnsealed, getChannelSeals, getContentKey } from 'context/sealUtil';
|
||||
import { isUnsealed, getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil';
|
||||
import { JSEncrypt } from 'jsencrypt'
|
||||
|
||||
import { decryptTopicSubject } from 'context/sealUtil';
|
||||
@ -23,6 +23,7 @@ export function useConversation(cardId, channelId) {
|
||||
loading: false,
|
||||
sealed: false,
|
||||
contentKey: null,
|
||||
busy: false,
|
||||
});
|
||||
|
||||
const profile = useContext(ProfileContext);
|
||||
@ -120,7 +121,17 @@ export function useConversation(cardId, channelId) {
|
||||
// eslint-disable-next-line
|
||||
}, [cardId, channelId]);
|
||||
|
||||
const syncTopic = async (item, value) => {
|
||||
useEffect(() => {
|
||||
syncChannel();
|
||||
// eslint-disable-next-line
|
||||
}, [conversation.state, profile.state, card.state]);
|
||||
|
||||
useEffect(() => {
|
||||
topics.current = new Map();
|
||||
syncChannel();
|
||||
}, [state.contentKey]);
|
||||
|
||||
const syncTopic = (item, value) => {
|
||||
const revision = value.data?.detailRevision;
|
||||
const detail = value.data?.topicDetail || {};
|
||||
const identity = profile.state.identity || {};
|
||||
@ -172,42 +183,34 @@ export function useConversation(cardId, channelId) {
|
||||
}
|
||||
}
|
||||
|
||||
if (detail.dataType === 'superbasictopic') {
|
||||
if (item.revision !== revision) {
|
||||
try {
|
||||
if (item.revision !== revision) {
|
||||
try {
|
||||
if (detail.dataType === 'superbasictopic') {
|
||||
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;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (detail.dataType === 'sealedtopic') {
|
||||
if (item.revision !== revision || item.contentKey !== state.contentKey) {
|
||||
item.contentKey = state.contentKey;
|
||||
try {
|
||||
if (detail.dataType === 'sealedtopic') {
|
||||
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;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
item.revision = revision;
|
||||
}
|
||||
item.transform = detail.transform;
|
||||
item.status = detail.status;
|
||||
item.assetUrl = conversation.actions.getTopicAssetUrl;
|
||||
item.revision = revision;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const syncChannel = () => {
|
||||
const messages = new Map();
|
||||
conversation.state.topics.forEach((value, id) => {
|
||||
const curCardId = conversation.state.card?.id;
|
||||
@ -233,8 +236,7 @@ export function useConversation(cardId, channelId) {
|
||||
});
|
||||
|
||||
updateState({ topics: sorted });
|
||||
// eslint-disable-next-line
|
||||
}, [conversation.state, profile.state, card.state, state.contentKey]);
|
||||
}
|
||||
|
||||
const actions = {
|
||||
more: () => {
|
||||
@ -251,6 +253,34 @@ export function useConversation(cardId, channelId) {
|
||||
removeTopic: async (topicId) => {
|
||||
await conversation.actions.removeTopic(topicId);
|
||||
},
|
||||
updateTopic: async (topic, text) => {
|
||||
const { assets, textSize, textColor } = topic;
|
||||
const message = { text, textSize, textColor, assets };
|
||||
console.log("UPDATE", message);
|
||||
|
||||
if (!state.busy) {
|
||||
updateState({ busy: true });
|
||||
try {
|
||||
if (state.sealed) {
|
||||
if (state.contentKey) {
|
||||
const subject = encryptTopicSubject({ message }, state.contentKey);
|
||||
await conversation.actions.setTopicSubject(topic.id, 'sealedtopic', subject);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await conversation.actions.setTopicSubject(topic.id, 'superbasictopic', message);
|
||||
}
|
||||
updateState({ busy: false });
|
||||
}
|
||||
catch(err) {
|
||||
updateState({ busy: false });
|
||||
throw new Error("topic update failed");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error("operation in progress");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return { state, actions };
|
||||
|
Loading…
Reference in New Issue
Block a user