chat refactoring, wip group chat

This commit is contained in:
Martti Malmi 2023-07-28 18:46:08 +03:00
parent 0e1ea962c4
commit 4beee82679
11 changed files with 414 additions and 391 deletions

View File

@ -134,7 +134,6 @@ class Main extends Component<Props, ReactState> {
<Notifications path="/notifications" />
<Chat path="/chat/hashtag/:hashtag?" />
<Chat path="/chat/:id?" />
<Chat path="/chat/new/:id" />
<Note path="/post/:id+" />
<Torrent path="/torrent/:id+" />
<About path="/about" />

View File

@ -10,7 +10,7 @@ import Name from './user/Name';
import Torrent from './Torrent';
const PrivateMessage = (props) => {
const [text, setText] = useState('');
const [text, setText] = useState(props.text || '');
useEffect(() => {
$('a').click((e) => {
@ -20,9 +20,11 @@ const PrivateMessage = (props) => {
route(href.replace('https://iris.to/', ''));
}
});
Key.decryptMessage(props.id, (decryptedText) => {
setText(decryptedText);
});
if (!text) {
Key.decryptMessage(props.id, (decryptedText) => {
setText(decryptedText);
});
}
}, [props.id]);
const onNameClick = () => {

View File

@ -688,7 +688,7 @@ const Events = {
PubSub.publish(event as Event);
console.log('publishing event', event);
this.handle(event as Event);
this.handle(event as Event, true);
// also publish at most 10 events referred to in tags
const referredEvents = event.tags

View File

@ -1,12 +1,11 @@
import { PaperAirplaneIcon } from '@heroicons/react/24/solid';
import Show from '../../components/helpers/Show';
import Key from '../../nostr/Key';
import { translate as t } from '../../translations/Translation.mjs';
import View from '../View';
import ChatList from './ChatList';
import PrivateChat from './PrivateChat';
import ChatMessages from './ChatMessages';
import NewChat from './NewChat';
class Chat extends View {
id: string;
@ -17,22 +16,30 @@ class Chat extends View {
this.hideSideBar = true;
}
renderView() {
const hexId = Key.toNostrHexAddress(this.props.id) || undefined;
return (
<div className="flex flex-row">
<ChatList activeChat={hexId} className={hexId ? 'hidden md:flex' : 'flex'} />
<Show when={hexId}>
<PrivateChat id={hexId || ''} key={hexId} />
</Show>
<Show when={!hexId}>
<div className="hidden md:flex flex-col items-center justify-center flex-1">
<div className="my-4">
<PaperAirplaneIcon className="w-24 h-24 text-neutral-400" />
</div>
<div className="text-neutral-400">{t('dm_privacy_warning')}</div>
renderContent = (id) => {
if (id === 'new') {
return <NewChat />;
} else if (id) {
return <ChatMessages id={id} key={id} />;
} else {
return (
<div className="hidden md:flex flex-col items-center justify-center flex-1">
<div className="my-4">
<PaperAirplaneIcon className="w-24 h-24 text-neutral-400" />
</div>
</Show>
<div className="text-neutral-400">{t('dm_privacy_warning')}</div>
</div>
);
}
};
renderView() {
const { id } = this.props;
return (
<div className="flex flex-row h-full">
<ChatList activeChat={id} className={id ? 'hidden md:flex' : 'flex'} />
{this.renderContent(id)}
</div>
);
}

View File

@ -1,8 +1,11 @@
import { PlusIcon } from '@heroicons/react/24/solid';
import $ from 'jquery';
import { route } from 'preact-router';
import BaseComponent from '../../BaseComponent';
import localState from '../../LocalState';
import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import { translate as t } from '../../translations/Translation.mjs';
import ChatListItem from './ChatListItem';
@ -17,6 +20,26 @@ interface ChatListState {
sortedChats: Array<string>;
}
const NewChatButton = ({ active }) => (
<div
role="button"
tabIndex={0}
className={`flex p-2 flex-row gap-4 h-16 items-center cursor-pointer hover:bg-neutral-900 ${
active ? 'bg-neutral-700' : ''
}`}
onClick={() => route(`/chat/new`)}
>
<div className="flex justify-center items-center w-12 h-12 rounded-full">
<PlusIcon className="w-6 h-6" />
</div>
<div className="flex flex-row">
<div className="flex flex-col">
<span className="name">{t('new_chat')}</span>
</div>
</div>
</div>
);
class ChatList extends BaseComponent<ChatListProps, ChatListState> {
constructor(props: ChatListProps) {
super(props);
@ -68,7 +91,9 @@ class ChatList extends BaseComponent<ChatListProps, ChatListState> {
}
render() {
const activeChat = this.props.activeChat;
const activeChat =
(this.props.activeChat && Key.toNostrHexAddress(this.props.activeChat)) ||
this.props.activeChat;
return (
<section
@ -87,6 +112,7 @@ class ChatList extends BaseComponent<ChatListProps, ChatListState> {
</div>
</div>
<div className="flex flex-1 flex-col">
<NewChatButton active={activeChat === 'new'} />
{this.state.sortedChats.map((pubkey) => (
<ChatListItem
active={pubkey === activeChat}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
import $ from 'jquery';
import { getEventHash, getSignature, nip04 } from 'nostr-tools';
import BaseComponent from '../../BaseComponent';
import Helpers from '../../Helpers';
import localState from '../../LocalState';
import Events from '../../nostr/Events';
@ -12,24 +12,28 @@ interface ChatMessageFormProps {
class?: string;
autofocus?: boolean;
onSubmit?: () => void;
keyPair?: { pubKey: string; privKey: string };
}
class ChatMessageForm extends BaseComponent<ChatMessageFormProps> {
componentDidMount() {
if (!Helpers.isMobile && this.props.autofocus !== false) {
$(this.base).find('.new-msg').focus();
}
}
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
activeChat,
class: classProp,
autofocus,
onSubmit,
keyPair,
}) => {
const [message, setMessage] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
componentDidUpdate() {
if (!Helpers.isMobile && this.props.autofocus !== false) {
$(this.base).find('.new-msg').focus();
useEffect(() => {
if (!Helpers.isMobile && autofocus !== false) {
inputRef.current?.focus();
}
}
}, [autofocus]);
encrypt(text: string) {
const privateEncrypt = (text: string) => {
try {
const theirPub = Key.toNostrHexAddress(this.props.activeChat);
const theirPub = Key.toNostrHexAddress(activeChat);
if (!theirPub) {
throw new Error('invalid public key ' + theirPub);
}
@ -37,73 +41,77 @@ class ChatMessageForm extends BaseComponent<ChatMessageFormProps> {
} catch (e) {
console.error(e);
}
}
};
async onSubmit(e: Event) {
const handleSubmit = async (e) => {
e.preventDefault();
e.stopPropagation();
const textEl = $(this.base).find('.new-msg');
const text = textEl.val() as string;
if (!text.length) {
if (!message.length) {
return;
}
const content = await this.encrypt(text);
const recipient = Key.toNostrHexAddress(this.props.activeChat);
if (!recipient) {
throw new Error('invalid public key ' + recipient);
const event = { kind: 4, created_at: Math.floor(Date.now() / 1000) } as any;
if (keyPair) {
// group message
event.content = await nip04.encrypt(keyPair.privKey, keyPair.pubKey, message);
event.pubkey = keyPair.pubKey;
event.tags = [['p', keyPair.pubKey]];
console.log('event', event);
event.id = getEventHash(event);
event.sig = getSignature(event, keyPair.privKey);
} else {
const recipient = Key.toNostrHexAddress(activeChat);
event.content = await privateEncrypt(message);
event.tags = [['p', recipient]];
if (!recipient) {
throw new Error('invalid public key ' + recipient);
}
}
Events.publish({
kind: 4,
content,
tags: [['p', recipient]],
});
textEl.val('');
this.props.onSubmit?.();
}
Events.publish(event);
onMsgTextInput(event: Event) {
localState
.get('channels')
.get(this.props.activeChat)
.get('msgDraft')
.put($(event.target).val() as string);
}
setMessage('');
onKeyDown(e: KeyboardEvent) {
onSubmit?.();
};
const handleInputChange = (e) => {
const value = e.target.value;
setMessage(value);
localState.get('channels').get(activeChat).get('msgDraft').put(value);
};
const handleKeyDown = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
this.onSubmit(e as Event);
handleSubmit(e as any);
}
}
};
render() {
return (
<form
autocomplete="off"
class={`flex flex-1 flex-row gap-2 p-2 message-form sticky w-full bottom-0 w-96 max-w-screen bg-black ${
this.props.class || ''
}`}
onSubmit={(e: Event) => this.onSubmit(e)}
>
<input
className="input input-sm flex-1 new-msg"
onInput={(e: Event) => this.onMsgTextInput(e)}
onKeyDown={(e: KeyboardEvent) => this.onKeyDown(e)}
type="text"
placeholder="Type a message"
autocomplete="off"
autocorrect="off"
autocapitalize="sentences"
spellCheck={true}
/>
<button className="btn btn-neutral btn-sm" style={{ marginRight: '0' }}>
<PaperAirplaneIcon onClick={(e: MouseEvent) => this.onSubmit(e as Event)} width="24" />
</button>
</form>
);
}
}
return (
<form
autoComplete="off"
className={`flex flex-1 flex-row gap-2 p-2 message-form sticky w-full bottom-0 w-96 max-w-screen bg-black ${
classProp || ''
}`}
onSubmit={handleSubmit}
>
<input
ref={inputRef}
className="input input-sm flex-1 new-msg"
onInput={handleInputChange}
onKeyDown={handleKeyDown}
type="text"
placeholder="Type a message"
autoComplete="off"
autoCorrect="off"
autoCapitalize="sentences"
spellCheck={true}
value={message}
/>
<button className="btn btn-neutral btn-sm" style={{ marginRight: '0' }}>
<PaperAirplaneIcon width="24" />
</button>
</form>
);
};
export default ChatMessageForm;

View File

@ -0,0 +1,243 @@
import { Helmet } from 'react-helmet';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import $ from 'jquery';
import throttle from 'lodash/throttle';
import { getPublicKey, nip04 } from 'nostr-tools';
import { useEffect, useRef, useState } from 'preact/hooks';
import Show from '../../components/helpers/Show';
import PrivateMessage from '../../components/PrivateMessage';
import Helpers from '../../Helpers';
import localState from '../../LocalState';
import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import PubSub from '../../nostr/PubSub';
import { translate as t } from '../../translations/Translation.mjs';
import ChatMessageForm from './ChatMessageForm.tsx';
function ChatMessages({ id }) {
const ref = useRef(null);
const [sortedMessages, setSortedMessages] = useState([] as any[]);
const [stickToBottom, setStickToBottom] = useState(true);
const [keyPair, setKeyPair] = useState(
undefined as { pubKey: string; privKey: string } | undefined,
);
let unsub;
let messageViewScrollHandler;
const addFloatingDaySeparator = () => {
let currentDaySeparator = $('.day-separator').last();
let pos = currentDaySeparator.position();
while (currentDaySeparator && pos && pos.top - 55 > 0) {
currentDaySeparator = currentDaySeparator.prevAll('.day-separator').first();
pos = currentDaySeparator.position();
}
const s = currentDaySeparator.clone();
const center = $('<div>')
.css({ position: 'fixed', top: 70, 'text-align': 'center' })
.attr('id', 'floating-day-separator')
.width($('#message-view').width())
.append(s);
$('#floating-day-separator').remove();
setTimeout(() => s.fadeOut(), 2000);
$('#message-view').prepend(center);
};
const toggleScrollDownBtn = () => {
const el = $('#message-view');
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() <= el.outerHeight() + 200;
if (scrolledToBottom) {
$('#scroll-down-btn:visible').fadeOut(150);
} else {
$('#scroll-down-btn:not(:visible)').fadeIn(150);
}
};
const onMessageViewScroll = () => {
messageViewScrollHandler =
messageViewScrollHandler ||
throttle(() => {
if ($('#attachment-preview:visible').length) {
return;
}
addFloatingDaySeparator();
toggleScrollDownBtn();
}, 200);
messageViewScrollHandler();
};
const scrollDown = () => {
Helpers.scrollToMessageListBottom();
const el = document.getElementById('message-list');
el && (el.style.paddingBottom = '0');
};
const renderMainView = () => {
let mainView;
const myPub = Key.getPubKey();
const now = new Date();
const nowStr = now.toLocaleDateString();
let previousDateStr;
let previousFrom;
const msgListContent = [] as any[];
if (id && id.length > 4) {
sortedMessages.forEach((msgOrId) => {
let msg;
if (typeof msgOrId === 'string') {
msg = Events.db.by('id', msgOrId);
} else {
msg = msgOrId;
}
if (!msg) {
return null;
}
const date = new Date(msg.created_at * 1000);
let isDifferentDay;
if (date) {
const dateStr = date.toLocaleDateString();
if (dateStr !== previousDateStr) {
isDifferentDay = true;
const separatorText = Helpers.getDaySeparatorText(date, dateStr, now, nowStr);
msgListContent.push(
<div className="px-2 py-1 inline-block day-separator bg-black opacity-50 text-white rounded-full">
{t(separatorText.toLowerCase())}
</div>,
);
}
previousDateStr = dateStr;
}
let showName = false;
if (
msg.pubkey !== myPub &&
(isDifferentDay || (previousFrom && msg.pubkey !== previousFrom))
) {
msgListContent.push(<div className="from-separator" />);
showName = true;
}
previousFrom = msg.pubkey;
msgListContent.push(
<PrivateMessage
{...msg}
showName={showName}
selfAuthored={msg.pubkey === myPub}
key={`${msg.created_at}${msg.pubkey}`}
chatId={id}
/>,
);
});
mainView = (
<div
className="main-view p-2 h-screen overflow-y-scroll overflow-x-hidden"
id="message-view"
onScroll={() => onMessageViewScroll()}
>
<div id="message-list">
{msgListContent}
<div className="italic my-2 text-neutral-500 w-full text-center">
{t('dm_privacy_warning')}
</div>
</div>
<div
id="attachment-preview"
className="attachment-preview"
style={{ display: 'none' }}
></div>
</div>
);
}
return mainView;
};
const messagesById = new Map<string, any>();
const renderMsgForm = () => {
return (
<>
<div id="scroll-down-btn" style={{ display: 'none' }} onClick={() => scrollDown()}>
<ChevronDownIcon width="24" />
</div>
<ChatMessageForm key={id} activeChat={id} onSubmit={() => scrollDown()} keyPair={keyPair} />
</>
);
};
useEffect(() => {
const hexId = Key.toNostrHexAddress(id);
const subscribePrivate = (hexId) => {
unsub = PubSub.subscribe({ kinds: [4], '#p': [Key.getPubKey()], authors: [hexId] });
Events.getDirectMessagesByUser(hexId, (msgIds) => {
if (msgIds) {
setSortedMessages(msgIds.reverse());
}
});
};
const subscribePublic = () => {
localState.get(`groups/${id}/key`).on((privKey) => {
console.log('group private key', privKey);
const pubKey = getPublicKey(privKey);
console.log('group public key', pubKey);
setKeyPair({ privKey, pubKey });
unsub = PubSub.subscribe(
{ kinds: [4], '#p': [pubKey], authors: [pubKey] },
async (event) => {
console.log('got group message', event);
const decrypted = await nip04.decrypt(privKey, pubKey, event.content);
messagesById.set(event.id, { ...event, text: decrypted });
setSortedMessages(Array.from(messagesById.values()));
},
);
});
};
hexId ? subscribePrivate(hexId) : subscribePublic();
const container = document.getElementById('message-list');
if (container) {
container.style.paddingBottom = '0';
container.style.paddingTop = '0';
const el = $('#message-view');
el.off('scroll').on('scroll', () => {
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() == el.outerHeight();
if (stickToBottom && !scrolledToBottom) {
setStickToBottom(false);
} else if (!stickToBottom && scrolledToBottom) {
setStickToBottom(true);
}
});
}
return () => {
unsub && unsub();
};
}, [id]);
useEffect(() => {
if (stickToBottom) {
Helpers.scrollToMessageListBottom();
}
$('.msg-content img')
.off('load')
.on('load', () => stickToBottom && Helpers.scrollToMessageListBottom());
}, [stickToBottom]);
return (
<>
<Helmet>
<title>{'Messages'}</title>
</Helmet>
<div id="chat-main" ref={ref} className={`${id ? '' : 'hidden'} flex-1 pb-12`}>
{renderMainView()}
<Show when={id && id.length > 4}>{renderMsgForm()}</Show>
</div>
</>
);
}
export default ChatMessages;

View File

View File

@ -0,0 +1,34 @@
import { generatePrivateKey } from 'nostr-tools';
import { route } from 'preact-router';
import localState from '../../LocalState';
import { translate as t } from '../../translations/Translation.mjs';
export default function NewChat() {
const startNewGroup = () => {
const randomChatID = Math.floor(Math.random() * 1000000000);
const newNostrKey = generatePrivateKey();
localState.get(`groups/${randomChatID}/key`).put(newNostrKey);
route(`/chat/${randomChatID}`);
};
return (
<div className="flex flex-1 flex-col items-center justify-center h-full">
<button className="btn btn-primary" onClick={startNewGroup}>
{t('start_new_group')}
</button>
<div className="my-4">{t('or')}</div>
<div className="my-4 flex gap-2 justify-center items-center">
<input
placeholder="Paste a chat link"
type="text"
id="pasteLink"
className="text-center input border border-gray-400 rounded p-2"
/>
<button id="scanQR" className="btn btn-neutral">
{t('Scan QR Code')}
</button>
</div>
</div>
);
}

View File

@ -1,285 +0,0 @@
import { createRef } from 'react';
import { Helmet } from 'react-helmet';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import $ from 'jquery';
import throttle from 'lodash/throttle';
import { PureComponent } from 'preact/compat';
import PrivateMessage from '../../components/PrivateMessage';
import Helpers from '../../Helpers';
import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import PubSub from '../../nostr/PubSub';
import Session from '../../nostr/Session';
import { translate as t } from '../../translations/Translation.mjs';
import ChatMessageForm from './ChatMessageForm.tsx';
interface PrivateChatState {
decryptedMessages: Record<string, any>;
sortedMessages: any[];
sortedParticipants: any[];
showParticipants: boolean;
stickToBottom: boolean;
noLongerParticipant: boolean;
}
interface PrivateChatProps {
id: string;
}
class PrivateChat extends PureComponent<PrivateChatProps, PrivateChatState> {
unsub: any;
chat: any;
ref: any;
messageViewScrollHandler: any;
constructor(props: PrivateChatProps) {
super(props);
this.ref = createRef();
this.state = {
decryptedMessages: {},
sortedMessages: [],
sortedParticipants: [],
showParticipants: true,
stickToBottom: true,
noLongerParticipant: false,
};
}
shouldComponentUpdate() {
return true;
}
updateLastOpened() {
const hexId = Key.toNostrHexAddress(this.props.id);
Session.public?.set('chats/' + hexId + '/lastOpened', Math.floor(Date.now() / 1000));
}
componentDidMount() {
const hexId = Key.toNostrHexAddress(this.props.id);
if (!hexId) {
console.error('no id');
return;
}
this.unsub = PubSub.subscribe({ kinds: [4], '#p': [Key.getPubKey()], authors: [hexId] });
Events.getDirectMessagesByUser(hexId, (msgIds) => {
if (msgIds) {
this.setState({ sortedMessages: msgIds.reverse() });
}
});
this.updateLastOpened();
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.updateLastOpened();
}
});
const container = document.getElementById('message-list');
if (container) {
container.style.paddingBottom = '0';
container.style.paddingTop = '0';
const el = $('#message-view');
el.off('scroll').on('scroll', () => {
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() == el.outerHeight();
if (this.state.stickToBottom && !scrolledToBottom) {
this.setState({ stickToBottom: false });
} else if (!this.state.stickToBottom && scrolledToBottom) {
this.setState({ stickToBottom: true });
}
});
}
}
componentWillUnmount() {
document.removeEventListener('visibilitychange', () => {
if (!document.hidden) {
this.updateLastOpened();
}
});
this.unsub && this.unsub();
}
componentDidUpdate(previousProps: any) {
if (this.state.stickToBottom) {
Helpers.scrollToMessageListBottom();
}
if (previousProps.id !== this.props.id) {
this.updateLastOpened();
}
$('.msg-content img')
.off('load')
.on('load', () => this.state.stickToBottom && Helpers.scrollToMessageListBottom());
setTimeout(() => {
if (
this.chat &&
!this.chat.uuid &&
Key.toNostrHexAddress(this.props.id) !== Key.getPubKey()
) {
if ($('.msg.our').length && !$('.msg.their').length && !this.chat.theirMsgsLastSeenTime) {
$('#not-seen-by-them').slideDown();
} else {
$('#not-seen-by-them').slideUp();
}
}
}, 2000);
}
// Original Preact methods converted to TypeScript with React JSX
addFloatingDaySeparator() {
let currentDaySeparator = $('.day-separator').last();
let pos = currentDaySeparator.position();
while (currentDaySeparator && pos && pos.top - 55 > 0) {
currentDaySeparator = currentDaySeparator.prevAll('.day-separator').first();
pos = currentDaySeparator.position();
}
const s = currentDaySeparator.clone();
const center = $('<div>')
.css({ position: 'fixed', top: 70, 'text-align': 'center' })
.attr('id', 'floating-day-separator')
.width($('#message-view').width())
.append(s);
$('#floating-day-separator').remove();
setTimeout(() => s.fadeOut(), 2000);
$('#message-view').prepend(center);
}
toggleScrollDownBtn() {
const el = $('#message-view');
const scrolledToBottom = el[0].scrollHeight - el.scrollTop() <= el.outerHeight() + 200;
if (scrolledToBottom) {
$('#scroll-down-btn:visible').fadeOut(150);
} else {
$('#scroll-down-btn:not(:visible)').fadeIn(150);
}
}
onMessageViewScroll() {
this.messageViewScrollHandler =
this.messageViewScrollHandler ||
throttle(() => {
if ($('#attachment-preview:visible').length) {
return;
}
this.addFloatingDaySeparator();
this.toggleScrollDownBtn();
}, 200);
this.messageViewScrollHandler();
}
scrollDown() {
Helpers.scrollToMessageListBottom();
const el = document.getElementById('message-list');
el && (el.style.paddingBottom = '0');
}
renderMainView() {
let mainView;
if (this.props.id && this.props.id.length > 20) {
const myPub = Key.getPubKey();
const now = new Date();
const nowStr = now.toLocaleDateString();
let previousDateStr;
let previousFrom;
const msgListContent: any[] = [];
this.state.sortedMessages.forEach((msgId) => {
const msg = Events.db.by('id', msgId);
if (!msg) {
return null;
}
const date = new Date(msg.created_at * 1000);
let isDifferentDay;
if (date) {
const dateStr = date.toLocaleDateString();
if (dateStr !== previousDateStr) {
isDifferentDay = true;
const separatorText = Helpers.getDaySeparatorText(date, dateStr, now, nowStr);
msgListContent.push(
<div className="px-2 py-1 inline-block day-separator bg-black opacity-50 text-white rounded-full">
{t(separatorText.toLowerCase())}
</div>,
);
}
previousDateStr = dateStr;
}
let showName = false;
if (
msg.pubkey !== myPub &&
(isDifferentDay || (previousFrom && msg.pubkey !== previousFrom))
) {
msgListContent.push(<div className="from-separator" />);
showName = true;
}
previousFrom = msg.pubkey;
msgListContent.push(
<PrivateMessage
{...msg}
showName={showName}
selfAuthored={msg.pubkey === myPub}
key={`${msg.created_at}${msg.pubkey}`}
chatId={this.props.id}
/>,
);
});
mainView = (
<div
className="main-view p-2 h-screen overflow-y-scroll overflow-x-hidden"
id="message-view"
onScroll={() => this.onMessageViewScroll()}
>
<div id="message-list">
{msgListContent}
<div className="italic my-2 text-neutral-500">{t('dm_privacy_warning')}</div>
</div>
<div
id="attachment-preview"
className="attachment-preview"
style={{ display: 'none' }}
></div>
</div>
);
}
return mainView;
}
renderMsgForm() {
return this.props.id && this.props.id.length > 20 ? (
<>
<div id="scroll-down-btn" style={{ display: 'none' }} onClick={() => this.scrollDown()}>
<ChevronDownIcon width="24" />
</div>
<ChatMessageForm
key={this.props.id}
activeChat={this.props.id}
onSubmit={() => this.scrollDown()}
/>
</>
) : (
''
);
}
render() {
return (
<>
<Helmet>
<title>{(this.chat && this.chat.name) || 'Messages'}</title>
</Helmet>
<div
id="chat-main"
ref={this.ref}
className={`${this.props.id ? '' : 'hidden'} flex-1 pb-12`}
>
{this.renderMainView()} {this.renderMsgForm()}
</div>
</>
);
}
}
export default PrivateChat;

View File

@ -1,11 +0,0 @@
import Component from '../../../BaseComponent';
class MainView extends Component {
constructor() {
super();
}
render() {
return '';
}
}
export default MainView;