adding dark mode and translation to thread component

This commit is contained in:
Roland Osborne 2024-03-02 21:29:18 -08:00
parent 71d6598556
commit 3629f30029
11 changed files with 202 additions and 125 deletions

View File

@ -149,6 +149,17 @@ export const en = {
webPassword: 'WebRTC Password',
failedLoad: 'Failed to Load',
limit: 'Limit',
deleteMessage: 'Deleting Message',
messageHint: 'Are you sure you want to delete the message?',
newMessage: 'New Message',
attachImage: 'Attach Image',
attachVideo: 'Attach Video',
attachAudio: 'Attach Audio',
attachFile: 'Attach File',
fontColor: 'Change Font Color',
fontSize: 'Change Font Size',
postMessage: 'Post Message',
};
export const fr = {
@ -302,5 +313,16 @@ export const fr = {
webPassword: 'Mot de Passe WebRTC',
failedLoad: 'Échec du Chargement',
limit: 'Limite',
deleteMessage: 'Suppression du Message',
messageHint: 'Êtes-vous Sûr de Vouloir Supprimer le Message?',
newMessage: 'Nouveau Message',
attachImage: 'Joindre une Image',
attachVideo: 'Joindre une Vidéo',
attachAudio: 'Joindre un Audio',
attachFile: 'Joindre un Fichier',
fontColor: 'Changer la Couleur du Message',
fontSize: 'Changer la Taille du Message',
postMessage: 'Publier le Message',
};

View File

@ -18,6 +18,8 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
remove={() => actions.removeTopic(topic.id)}
update={(text) => actions.updateTopic(topic, text)}
sealed={state.sealed && !state.contentKey}
strings={state.strings}
menuStyle={state.menuStyle}
/>)
}
@ -43,9 +45,9 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
return (
<ConversationWrapper>
<ChannelHeader openDetails={openDetails} closeConversation={closeConversation} contentKey={state.contentKey}/>
<div class="thread" ref={thread} onScroll={scrollThread}>
<div className="thread" ref={thread} onScroll={scrollThread}>
{ state.delayed && state.topics.length === 0 && (
<div class="empty">This Topic Has No Messages</div>
<div className="empty">This Topic Has No Messages</div>
)}
{ state.topics.length !== 0 && (
<ReactResizeDetector handleHeight={true}>
@ -59,34 +61,34 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
</ReactResizeDetector>
)}
{ state.loadingInit && (
<div class="loading">
<div className="loading">
<Spin size="large" delay={250} />
</div>
)}
{ state.loadingMore && (
<div class="loading">
<div className="loading">
<Spin size="large" delay={500} />
</div>
)}
</div>
<div class="divider">
<div class="line" />
<div className="divider">
<div className="line" />
{ state.uploadError && (
<div class="progress-error" />
<div className="progress-error" />
)}
{ state.upload && !state.uploadError && (
<div class="progress-active" style={{ width: state.uploadPercent + '%' }} />
<div className="progress-active" style={{ width: state.uploadPercent + '%' }} />
)}
{ !state.upload && (
<div class="progress-idle" />
<div className="progress-idle" />
)}
</div>
<div class="topic">
<div className="topic">
{ (!state.sealed || state.contentKey) && (
<AddTopic contentKey={state.contentKey} />
<AddTopic contentKey={state.contentKey} strings={state.strings} menuStyle={state.menuStyle} />
)}
{ state.uploadError && (
<div class="upload-error">
<div className="upload-error">
{ state.display === 'small' && (
<StatusError>
<div onClick={() => actions.clearUploadErrors(cardId, channelId)}>

View File

@ -1,18 +1,17 @@
import styled from 'styled-components';
import { Colors } from 'constants/Colors';
export const ConversationWrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: ${Colors.profileForm};
background-color: ${props => props.theme.selectedArea};
.header {
margin-left: 16px;
margin-right: 16px;
height: 48px;
border-bottom: 1px solid ${Colors.profileDivider};
border-bottom: 1px solid ${props => props.theme.headerBorder};
display: flex;
flex-direction: row;
align-items: center;
@ -27,6 +26,7 @@ export const ConversationWrapper = styled.div`
flex-direction: row;
align-items: center;
min-width: 0;
color: ${props => props.theme.mainText};
.label {
padding-left: 8px;
@ -43,7 +43,7 @@ export const ConversationWrapper = styled.div`
.button {
font-size: 18px;
color: ${Colors.grey};
color: ${props => props.theme.hintText};
cursor: pointer;
padding-right: 16px;
padding-left: 16px;
@ -65,7 +65,7 @@ export const ConversationWrapper = styled.div`
align-items: center;
justify-content: center;
font-size: 20;
color: ${Colors.grey};
color: ${props => props.theme.hintText};
}
.loading {
@ -85,21 +85,21 @@ export const ConversationWrapper = styled.div`
padding-right: 16px;
.line {
border-top: 1px solid ${Colors.divider};
border-top: 1px solid ${props => props.theme.itemBorder};
}
.progress-idle {
border-top: 1px solid ${Colors.divider};
border-top: 1px solid ${props => props.theme.itemBorder};
height: 1px;
}
.progress-active {
border-top: 1px solid ${Colors.primary};
border-top: 1px solid ${props => props.theme.linkText};
height: 1px;
}
.progress-error {
border-top: 1px solid ${Colors.alert};
border-top: 1px solid ${props => props.theme.alertText};
width: 100%;
height: 1px;
display: flex;
@ -122,7 +122,7 @@ export const ConversationWrapper = styled.div`
`
export const StatusError = styled.div`
color: ${Colors.error};
color: ${props => props.theme.alertText};
font-size: 14px;
padding-left: 8px;
cursor: pointer;

View File

@ -1,6 +1,6 @@
import { AddTopicWrapper } from './AddTopic.styled';
import { useAddTopic } from './useAddTopic.hook';
import { Modal, Input, Menu, Dropdown, Spin } from 'antd';
import { Modal, Tooltip, Input, Menu, Dropdown, Spin } from 'antd';
import { useRef } from 'react';
import { FieldBinaryOutlined, SoundOutlined, VideoCameraOutlined, PictureOutlined, FontColorsOutlined, FontSizeOutlined, SendOutlined } from '@ant-design/icons';
import { SketchPicker } from "react-color";
@ -10,7 +10,7 @@ import { BinaryFile } from './binaryFile/BinaryFile';
import { Carousel } from 'carousel/Carousel';
import { Gluejar } from '@charliewilco/gluejar'
export function AddTopic({ contentKey }) {
export function AddTopic({ contentKey, strings, menuStyle }) {
const { state, actions } = useAddTopic(contentKey);
@ -35,9 +35,9 @@ export function AddTopic({ contentKey }) {
catch (err) {
console.log(err);
modal.error({
title: 'Failed to Post Message',
content: 'Please try again.',
bodyStyle: { padding: 16 },
title: <span style={menuStyle}>{strings.operationFailed}</span>,
content: <span style={menuStyle}>{strings.tryAgain}</span>,
bodyStyle: { borderRadius: 8, padding: 16, ...menuStyle },
});
}
}
@ -121,47 +121,60 @@ export function AddTopic({ contentKey }) {
<input type='file' name="asset" accept="video/*" ref={attachVideo} onChange={e => onSelectVideo(e)} style={{display: 'none'}}/>
<input type='file' name="asset" accept="*/*" ref={attachBinary} onChange={e => onSelectBinary(e)} style={{display: 'none'}}/>
{ state.assets.length > 0 && (
<div class="assets">
<div className="assets">
<Carousel pad={32} items={state.assets} itemRenderer={renderItem} itemRemove={removeItem} />
</div>
)}
<div class="message">
<Input.TextArea ref={msg} placeholder="New Message" spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
<div className="message">
<Input.TextArea ref={msg} placeholder={strings.newMessage} spellCheck="true" autoSize={{ minRows: 2, maxRows: 6 }}
enterkeyhint="send" onKeyDown={(e) => keyDown(e)} onChange={(e) => actions.setMessageText(e.target.value)}
value={state.messageText} autocapitalize="none" />
</div>
<div class="buttons">
<div className="buttons">
{ state.enableImage && (
<div class="button space" onClick={() => attachImage.current.click()}>
<Tooltip placement="top" title={strings.attachImage}>
<div className="button space" onClick={() => attachImage.current.click()}>
<PictureOutlined />
</div>
</Tooltip>
)}
{ state.enableVideo && (
<div class="button space" onClick={() => attachVideo.current.click()}>
<Tooltip placement="top" title={strings.attachVideo}>
<div className="button space" onClick={() => attachVideo.current.click()}>
<VideoCameraOutlined />
</div>
</Tooltip>
)}
{ state.enableAudio && (
<div class="button space" onClick={() => attachAudio.current.click()}>
<Tooltip placement="top" title={strings.attachAudio}>
<div className="button space" onClick={() => attachAudio.current.click()}>
<SoundOutlined />
</div>
</Tooltip>
)}
<div class="button space" onClick={() => attachBinary.current.click()}>
<Tooltip placement="top" title={strings.attachFile}>
<div className="button space" onClick={() => attachBinary.current.click()}>
<FieldBinaryOutlined />
</div>
<div class="bar space" />
<div class="button space">
</Tooltip>
<div className="bar space" />
<div className="button space">
<Tooltip placement="top" title={strings.fontColor}>
<Dropdown overlay={picker} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
<FontColorsOutlined />
</Dropdown>
</Tooltip>
</div>
<div class="button space">
<div className="button space">
<Tooltip placement="top" title={strings.fontSize}>
<Dropdown overlay={sizer} overlayStyle={{ minWidth: 0 }} trigger={['click']} placement="top">
<FontSizeOutlined />
</Dropdown>
</Tooltip>
</div>
<div class="end">
<div class="button" onClick={addTopic}>
<div className="end">
<Tooltip placement="top" title={strings.postMessage}>
<div className="button" onClick={addTopic}>
{ state.busy && (
<Spin size="small" />
)}
@ -169,6 +182,7 @@ export function AddTopic({ contentKey }) {
<SendOutlined />
)}
</div>
</Tooltip>
</div>
</div>
</AddTopicWrapper>

View File

@ -1,10 +1,22 @@
import styled from 'styled-components';
import { Colors } from 'constants/Colors';
export const AddTopicWrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
background-color: ${props => props.theme.selectedArea};
color: ${props => props.theme.mainText};
textarea {
padding-left: 8px;
background-color: ${props => props.theme.inputArea};
border: 1px solid ${props => props.theme.sectionBorder};
color: ${props => props.theme.mainText};
}
textarea::placeholder {
color: ${props => props.theme.placeholderText};
}
.message {
width: 100%;
@ -30,7 +42,7 @@ export const AddTopicWrapper = styled.div`
align-items: center;
.bar {
border-left: 1px solid ${Colors.encircle};
border-left: 1px solid ${props => props.theme.sectionBorder};
height: 36px;
padding-right 8px;
margin-left: 8px;
@ -44,10 +56,10 @@ export const AddTopicWrapper = styled.div`
width: 36px;
height: 36px;
cursor: pointer;
border: 1px solid ${Colors.divider};
background-color: ${Colors.white};
border: 1px solid ${props => props.theme.sectionBorder};
background-color: ${props => props.theme.inputArea};
font-size: 18px;
color: ${Colors.enabled};
color: ${props => props.theme.descriptionText};
}
.space {

View File

@ -1,11 +1,10 @@
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};
border-bottom: 1px solid ${props => props.theme.headerBorder};
display: flex;
flex-direction: row;
align-items: center;
@ -22,6 +21,7 @@ export const ChannelHeaderWrapper = styled.div`
min-width: 0;
.label {
color: ${props => props.theme.mainText};
padding-left: 8px;
white-space: nowrap;
text-overflow: ellipsis;
@ -36,7 +36,7 @@ export const ChannelHeaderWrapper = styled.div`
.button {
font-size: 18px;
color: ${Colors.grey};
color: ${props => props.theme.hintText};
cursor: pointer;
padding-right: 16px;
padding-left: 16px;
@ -44,7 +44,7 @@ export const ChannelHeaderWrapper = styled.div`
`
export const StatusError = styled.div`
color: ${Colors.error};
color: ${props => props.theme.alertText};
font-size: 14px;
padding-left: 8px;
cursor: pointer;

View File

@ -9,18 +9,19 @@ import { ExclamationCircleOutlined, DeleteOutlined, EditOutlined, FireOutlined,
import { Carousel } from 'carousel/Carousel';
import { useTopicItem } from './useTopicItem.hook';
export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
export function TopicItem({ host, contentKey, sealed, topic, update, remove, strings, menuStyle }) {
const [ modal, modalContext ] = Modal.useModal();
const { state, actions } = useTopicItem(topic, contentKey);
const removeTopic = () => {
modal.confirm({
title: 'Do you want to delete this message?',
title: <span style={menuStyle}>{strings.deleteMessage}</span>,
content: <span style={menuStyle}>{strings.messageHint}</span>,
bodyStyle: { borderRadius: 8, padding: 16, ...menuStyle },
icon: <ExclamationCircleOutlined />,
bodyStyle: { padding: 16 },
okText: 'Yes, Delete',
cancelText: 'No, Cancel',
okText: strings.remove,
cancelText: strings.cancel,
onOk: async () => {
try {
await remove();
@ -28,9 +29,9 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
catch(err) {
console.log(err);
modal.error({
title: 'Failed to Delete Message',
content: 'Please try again.',
bodyStyle: { padding: 16 },
title: <span style={menuStyle}>{strings.operationFailed}</span>,
content: <span style={menuStyle}>{strings.tryAgain}</span>,
bodyStyle: { borderRadius: 8, padding: 16, ...menuStyle },
});
}
},
@ -45,9 +46,9 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
catch(err) {
console.log(err);
modal.error({
title: 'Failed to Update Message',
content: 'Please try again.',
bodyStyle: { padding: 16 },
title: <span style={menuStyle}>{strings.operationFailed}</span>,
content: <span style={menuStyle}>{strings.tryAgain}</span>,
bodyStyle: { borderRadius: 8, padding: 16, ...menuStyle },
});
}
};
@ -71,23 +72,23 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
return (
<TopicItemWrapper>
{ modalContext }
<div class="topic-header">
<div class="avatar">
<div className="topic-header">
<div className="avatar">
<Logo width={32} height={32} radius={4} url={topic.imageUrl} />
</div>
<div class="info">
<div class={ topic.nameSet ? 'set' : 'unset' }>{ topic.name }</div>
<div className="info">
<div className={ topic.nameSet ? 'set' : 'unset' }>{ topic.name }</div>
<div>{ topic.createdStr }</div>
</div>
<div class="topic-options">
<div class="buttons">
<div className="topic-options">
<div className="buttons">
{ !sealed && topic.creator && (
<div class="button edit" onClick={() => actions.setEditing(topic.text)}>
<div className="button edit" onClick={() => actions.setEditing(topic.text)}>
<EditOutlined />
</div>
)}
{ (host || topic.creator) && (
<div class="button remove" onClick={removeTopic}>
<div className="button remove" onClick={removeTopic}>
<DeleteOutlined />
</div>
)}
@ -95,7 +96,7 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
</div>
</div>
{ topic.status !== 'confirmed' && (
<div class="skeleton">
<div className="skeleton">
<Skeleton size={'small'} active={true} title={false} />
</div>
)}
@ -104,36 +105,36 @@ export function TopicItem({ host, contentKey, sealed, topic, update, remove }) {
{ topic.assets?.length && (
<>
{ topic.transform === 'error' && (
<div class="asset-placeholder">
<div className="asset-placeholder">
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
</div>
)}
{ topic.transform === 'incomplete' && (
<div class="asset-placeholder">
<div className="asset-placeholder">
<PictureOutlined style={{ fontSize: 32 }} />
</div>
)}
{ topic.transform === 'complete' && (
<div class="topic-assets">
<div className="topic-assets">
<Carousel pad={40} items={state.assets} itemRenderer={renderAsset} />
</div>
)}
</>
)}
{ sealed && (
<div class="sealed-message">sealed message</div>
<div className="sealed-message">sealed message</div>
)}
{ !sealed && !state.editing && (
<div class="message">
<div className="message">
<div style={{ color: topic.textColor, fontSize: topic.textSize }}>{ topic.clickable }</div>
</div>
)}
{ state.editing && (
<div class="editing">
<div className="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">
<div className="controls">
<Space>
<Button onClick={actions.clearEditing}>Cancel</Button>
<Button type="primary" onClick={updateTopic}>Save</Button>

View File

@ -1,10 +1,10 @@
import styled from 'styled-components';
import { Colors } from 'constants/Colors';
export const TopicItemWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
color: ${props => props.theme.mainText};
.topic-header {
display: flex;
@ -13,7 +13,7 @@ export const TopicItemWrapper = styled.div`
padding-left: 16px;
margin-right: 16px;
padding-top: 8px;
border-top: 1px solid #dddddd;
border-top: 1px solid ${props => props.theme.itemBorder};
&:hover .topic-options {
visibility: visible;
@ -29,7 +29,7 @@ export const TopicItemWrapper = styled.div`
display: flex;
flex-direction: row;
border-radius: 4px;
background-color: #eeeeee;
background-color: ${props => props.theme.modalArea};
margin-top: 2px;
.button {
@ -40,11 +40,11 @@ export const TopicItemWrapper = styled.div`
}
.remove {
color: ${Colors.warn};
color: ${props => props.theme.alertText};
}
.edit {
color: ${Colors.primary};
color: ${props => props.theme.linkText};
}
}
}
@ -63,18 +63,18 @@ export const TopicItemWrapper = styled.div`
.comments {
padding-left: 8px;
cursor: pointer;
color: #888888;
color: ${props => props.theme.descriptionText};
}
.set {
font-weight: bold;
color: #444444;
color: ${props => props.theme.mainText};
padding-right: 8px;
}
.unset {
font-weight: bold;
font-style: italic;
color: #888888;
color: ${props => props.theme.descriptionText};
padding-right: 8px;
}
.unknown {
@ -87,7 +87,7 @@ export const TopicItemWrapper = styled.div`
.sealed-message {
font-style: italic;
color: #aaaaaa;
color: ${props => props.theme.placeholderText};
padding-left: 72px;
}
@ -97,8 +97,8 @@ export const TopicItemWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
background-color: #eeeeee;
color: #888888;
background-color: ${props => props.theme.frameArea};
color: ${props => props.theme.placeholderText};
margin-left: 72px;
}
@ -118,18 +118,30 @@ export const TopicItemWrapper = styled.div`
padding-left: 72px;
white-space: pre-line;
min-height: 4px;
color: ${props => props.theme.mainText};
}
.editing {
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid #aaaaaa;
border: 1px solid ${props => props.theme.sectionBorder};
background-color: ${props => props.theme.inputArea};
margin-top: 8px;
margin-bottom: 8px;
margin-right: 16px;
margin-left: 72px;
textarea {
padding-left: 8px;
background-color: ${props => props.theme.inputArea};
color: ${props => props.theme.mainText};
}
textarea::placeholder {
color: ${props => props.theme.placeholderText};
}
.controls {
display: flex;
flex-direction: row;

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { fetchWithTimeout } from 'api/fetchUtil';
import { decryptBlock } from 'context/sealUtil';
export function useTopicItem(topic, contentKey) {
export function useTopicItem(topic, contentKey, strings, menuStyle) {
const [state, setState] = useState({
editing: false,

View File

@ -22,6 +22,9 @@ export function useConversation(cardId, channelId) {
sealed: false,
contentKey: null,
busy: false,
colors: {},
strings: {},
menuStyle: {},
});
const profile = useContext(ProfileContext);
@ -41,8 +44,9 @@ export function useConversation(cardId, channelId) {
}
useEffect(() => {
updateState({ display: settings.state.display });
}, [settings]);
const { strings, menuStyle, display, colors } = settings.state;
updateState({ strings, menuStyle, display, colors });
}, [settings.state]);
useEffect(() => {
const { dataType, data } = conversation.state.channel?.data?.channelDetail || {};
@ -125,7 +129,7 @@ export function useConversation(cardId, channelId) {
syncChannel();
// eslint-disable-next-line
}, [conversation.state, profile.state, card.state]);
}, [conversation.state, profile.state, card.state, settings.state]);
useEffect(() => {
topics.current = new Map();
@ -171,14 +175,29 @@ export function useConversation(cardId, channelId) {
const now = new Date();
const offset = now.getTime() - date.getTime();
if(offset < 86400000) {
item.createdStr = date.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'});
if (settings.state.timeFormat === '12h') {
item.createdStr = date.toLocaleTimeString("en-US", {hour: 'numeric', minute:'2-digit'});
}
else {
item.createdStr = date.toLocaleTimeString("en-GB", {hour: 'numeric', minute:'2-digit'});
}
}
else if (offset < 31449600000) {
if (settings.state.dateFormat === 'mm/dd') {
item.createdStr = date.toLocaleDateString("en-US", {day: 'numeric', month:'numeric'});
}
else {
item.createdStr = date.toLocaleDateString("en-GB", {day: 'numeric', month:'numeric'});
}
}
else {
if (settings.state.dateFormat === 'mm/dd') {
item.createdStr = date.toLocaleDateString("en-US");
}
else {
item.createdStr = date.toLocaleDateString("en-GB");
}
}
if (detail.guid === identity.guid) {
item.creator = true;
@ -188,7 +207,7 @@ export function useConversation(cardId, channelId) {
item.nameSet = true;
}
else {
item.name = identity.node ? `${identity.handle}@${identity.node}` : identity.handle ? identity.handle : 'unknown';
item.name = identity.node ? `${identity.handle}/${identity.node}` : identity.handle ? identity.handle : 'unknown';
item.nameSet = false;
}
}
@ -202,7 +221,7 @@ export function useConversation(cardId, channelId) {
item.nameSet = true;
}
else {
item.name = contact.node ? `${contact.handle}@${contact.node}` : contact.handle ? contact.handle : 'unknown';
item.name = contact.node ? `${contact.handle}/${contact.node}` : contact.handle ? contact.handle : 'unknown';
item.nameSet = false;
}
}
@ -220,7 +239,7 @@ export function useConversation(cardId, channelId) {
item.assets = message.assets;
item.text = message.text;
item.clickable = clickableText(message.text);
item.textColor = message.textColor ? message.textColor : '#444444';
item.textColor = message.textColor ? message.textColor : state.colors.mainText;
item.textSize = message.textSize ? message.textSize : 14;
}
if (detail.dataType === 'sealedtopic' && state.contentKey) {
@ -228,7 +247,7 @@ export function useConversation(cardId, channelId) {
item.assets = subject.message.assets;
item.text = subject.message.text;
item.clickable = clickableText(subject.message.text);
item.textColor = subject.message.textColor ? subject.message.textColor : '#444444';
item.textColor = subject.message.textColor ? subject.message.textColor : state.colors.mainText;
item.textSize = subject.message.textSize ? subject.message.textSize : 14;
}
}

5
todo
View File

@ -1,9 +1,4 @@
thread:
- dark style
- translation
- button tool tip
details:
- back button
- dark style