312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
|
|
import React, {Component, createRef, Fragment} from "react";
|
||
|
|
import {connect} from "react-redux";
|
||
|
|
import {Dispatch} from "redux";
|
||
|
|
import debounce from "lodash/debounce";
|
||
|
|
|
||
|
|
import {UserInfo} from "@/typings/user";
|
||
|
|
import {ChatboxTextMessage} from "@/typings/message.d";
|
||
|
|
import {sendTextMessageToServer} from "@/lib/messaging";
|
||
|
|
import {getCurrentUTCTimestamp} from "@/lib/utils";
|
||
|
|
import {getUserChatHistory} from "@/lib/api";
|
||
|
|
import {AppState, NetworkingStatusType} from "@/store/typing";
|
||
|
|
import {actions} from "@/store/actions";
|
||
|
|
import {NOTIFICATIONS} from "@/constant/text";
|
||
|
|
import {getAdminInfo} from "@/lib/user";
|
||
|
|
import storage, {userChatHistoryStorageKey} from "@/lib/storage";
|
||
|
|
|
||
|
|
import {MessageItem} from "./MessageItem";
|
||
|
|
import InputMessage from "./InputMessage";
|
||
|
|
import TypingNotification from "./TypingNotification";
|
||
|
|
|
||
|
|
import './Chatbox.css';
|
||
|
|
|
||
|
|
|
||
|
|
type ChatboxProps = {
|
||
|
|
user_info: UserInfo;
|
||
|
|
network_connection: NetworkingStatusType;
|
||
|
|
chat_messages: ChatboxTextMessage[];
|
||
|
|
dispatch: Dispatch;
|
||
|
|
}
|
||
|
|
|
||
|
|
type ChatboxState = {
|
||
|
|
loadingHistory: boolean,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/*function persistMessage(user_id: string, messages: any[]) {
|
||
|
|
const MAX_MESSAGE_KEPT_PER_USER: number = 200;
|
||
|
|
|
||
|
|
// limit messages per user
|
||
|
|
const total_message = messages.length;
|
||
|
|
const keep_from_index = total_message - MAX_MESSAGE_KEPT_PER_USER;
|
||
|
|
|
||
|
|
storage.save(userChatHistoryStorageKey(user_id), (keep_from_index > 0) ? messages.slice(keep_from_index) : messages);
|
||
|
|
}*/
|
||
|
|
|
||
|
|
|
||
|
|
class Chatbox extends Component<ChatboxProps, ChatboxState> {
|
||
|
|
|
||
|
|
static MAX_MESSAGE_KEPT_PER_USER: number = 200;
|
||
|
|
|
||
|
|
private trackLastScrollTop: number;
|
||
|
|
private disableScrollBottom: boolean;
|
||
|
|
private loadingHistoryStatus: 'idle' | 'loading' | 'done';
|
||
|
|
private noMoreHistoryMessage: boolean;
|
||
|
|
|
||
|
|
private scrollBottomDiv: React.RefObject<HTMLDivElement>;
|
||
|
|
private scrollToHistoryDiv: React.RefObject<HTMLDivElement>;
|
||
|
|
|
||
|
|
constructor(props: ChatboxProps) {
|
||
|
|
super(props);
|
||
|
|
this.state = {
|
||
|
|
loadingHistory: false
|
||
|
|
}
|
||
|
|
this.scrollBottomDiv = createRef<HTMLDivElement>();
|
||
|
|
this.scrollToHistoryDiv = createRef<HTMLDivElement>();
|
||
|
|
|
||
|
|
this.trackLastScrollTop = 0;
|
||
|
|
this.disableScrollBottom = false;
|
||
|
|
this.loadingHistoryStatus = 'idle';
|
||
|
|
this.noMoreHistoryMessage = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
componentDidMount() {
|
||
|
|
this.getChatHistory().then();
|
||
|
|
}
|
||
|
|
|
||
|
|
getSnapshotBeforeUpdate(prevProps: ChatboxProps, prevState: ChatboxState) {
|
||
|
|
// Are we adding new items to the list?
|
||
|
|
// Capture the scroll position so we can adjust scroll later.
|
||
|
|
if (prevProps.chat_messages.length < this.props.chat_messages.length) {
|
||
|
|
const div = this.scrollToHistoryDiv.current;
|
||
|
|
return (div) ? div.scrollHeight - div.scrollTop : null;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
componentDidUpdate(prevProps: ChatboxProps, prevState: ChatboxState, snapshot: number | null) {
|
||
|
|
console.log('componentDidUpdate at ' + new Date().getTime());
|
||
|
|
// snapshot is the result of getSnapshotBeforeUpdate()
|
||
|
|
if (snapshot !== null) {
|
||
|
|
this.scrollToHistoryDiv.current!.scrollTop = this.scrollToHistoryDiv.current!.scrollHeight - snapshot;
|
||
|
|
// this.scrollToHistoryDiv.current!.scrollTo({top: expected_top, behavior:'smooth'});
|
||
|
|
this.loadingHistoryStatus = 'idle';
|
||
|
|
}
|
||
|
|
|
||
|
|
if(prevProps.chat_messages.length !== this.props.chat_messages.length) {
|
||
|
|
this.scrollToBottom();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
getChatHistory = async (from_scroll: boolean = false) => {
|
||
|
|
|
||
|
|
console.log('Chatbox getChatHistory');
|
||
|
|
|
||
|
|
const {user_info, chat_messages, network_connection, dispatch} = this.props;
|
||
|
|
|
||
|
|
// todo: 15-April-2021
|
||
|
|
// because 1 user can chat with many admin staff, we need to remove all old messages in storage (if exist) when user first chat with this staff
|
||
|
|
if(network_connection === 'offline') {
|
||
|
|
// get for offline view only
|
||
|
|
const stored_messages: ChatboxTextMessage[] = await storage.get(userChatHistoryStorageKey(user_info.id)) || [];
|
||
|
|
console.log('stored_messages');
|
||
|
|
console.log(stored_messages);
|
||
|
|
dispatch(actions.addHistoryMessage({[user_info.id]: stored_messages}));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( chat_messages.length === 0 || from_scroll ) {
|
||
|
|
|
||
|
|
let last_fetch = (chat_messages.length > 0) ? chat_messages[0].time : 0;
|
||
|
|
|
||
|
|
this.setState({loadingHistory: true});
|
||
|
|
|
||
|
|
const old_messages = await getUserChatHistory({thread_id: user_info.id, last_fetch: last_fetch});
|
||
|
|
|
||
|
|
this.setState({loadingHistory: false});
|
||
|
|
|
||
|
|
if (old_messages.length === 0) {
|
||
|
|
console.log('old_messages: this.noMoreHistoryMessage = true')
|
||
|
|
this.noMoreHistoryMessage = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('dispatching historied messages');
|
||
|
|
dispatch(actions.addHistoryMessage({[user_info.id]: old_messages}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
scrollToBottom = () => {
|
||
|
|
// no scroll if user view history
|
||
|
|
if ( this.disableScrollBottom ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.scrollBottomDiv.current!.scrollIntoView({ behavior: "smooth" });
|
||
|
|
}
|
||
|
|
|
||
|
|
showNotification = () => {
|
||
|
|
const { user_info, network_connection } = this.props;
|
||
|
|
const NotiMessage = (txt: string, key: string|number) => (<div key={key}>{txt}</div>);
|
||
|
|
|
||
|
|
let list_messages: string[] = [];
|
||
|
|
|
||
|
|
// in-chat notification: user typing
|
||
|
|
if (network_connection === 'offline') {
|
||
|
|
list_messages.push(NOTIFICATIONS['network_offline']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// notify user offline
|
||
|
|
if (!user_info.online) {
|
||
|
|
list_messages.push(NOTIFICATIONS['user_offline']);
|
||
|
|
}
|
||
|
|
|
||
|
|
if(list_messages.length > 0) {
|
||
|
|
return (
|
||
|
|
<div style={{fontStyle:'italic', fontSize:11, textAlign: 'center'}} >
|
||
|
|
{
|
||
|
|
list_messages.map((txt, index) => NotiMessage(txt, index))
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// default nothing
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
handleChatboxScroll = debounce((event: any) => {
|
||
|
|
|
||
|
|
console.log('start handleChatboxScroll at ' + new Date().getSeconds());
|
||
|
|
//console.log(event);
|
||
|
|
|
||
|
|
if(!event.target) {
|
||
|
|
console.log('handleChatboxScroll: event.currentTarget');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if(this.noMoreHistoryMessage) {
|
||
|
|
console.log('handleChatboxScroll: noMoreHistoryMessage');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// user is scrolling up, so disable bottom scrolling for user to read old messages without interruption
|
||
|
|
if(this.trackLastScrollTop > event.target.scrollTop) {
|
||
|
|
this.disableScrollBottom = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// track again
|
||
|
|
this.trackLastScrollTop = event.target.scrollTop;
|
||
|
|
|
||
|
|
// load history on top
|
||
|
|
if (event.target.scrollTop === 0 && this.loadingHistoryStatus === 'idle') {
|
||
|
|
this.loadingHistoryStatus = 'loading';
|
||
|
|
this.disableScrollBottom = true;
|
||
|
|
console.log('handleChatboxScroll should get the history now ... ');
|
||
|
|
this.getChatHistory(true).then();
|
||
|
|
}
|
||
|
|
}, 300)
|
||
|
|
|
||
|
|
|
||
|
|
sendMessage = (typed_message: string) => {
|
||
|
|
//alert(typed_message);
|
||
|
|
const { user_info, dispatch, network_connection } = this.props; // , admin_info, user_info
|
||
|
|
|
||
|
|
//const {typed_message} = this.state;
|
||
|
|
if(typed_message === '') {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// reset when user send new message
|
||
|
|
if(this.disableScrollBottom) {
|
||
|
|
this.disableScrollBottom = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// cannot send message when network_connection is offline
|
||
|
|
// and restore message to the input
|
||
|
|
if(network_connection === 'offline') {
|
||
|
|
console.log("network_connection =offline");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// let composed_message = composeNewSendingMessage(txt);
|
||
|
|
// add locally
|
||
|
|
let composed_message = {
|
||
|
|
id: '',
|
||
|
|
from: 'me',
|
||
|
|
content: typed_message,
|
||
|
|
time: getCurrentUTCTimestamp(true),
|
||
|
|
// sequence: local_sequence,
|
||
|
|
deliveryStatus: 0,
|
||
|
|
} as ChatboxTextMessage;
|
||
|
|
|
||
|
|
dispatch(actions.addCurrentMessage({[user_info.id] : [composed_message]}));
|
||
|
|
|
||
|
|
// pass to networking layer to send to server
|
||
|
|
let send_to = (user_info.online) ? [user_info.id, user_info.node].join('-').trim() : '';
|
||
|
|
sendTextMessageToServer(send_to, typed_message);
|
||
|
|
}
|
||
|
|
|
||
|
|
showLoadingHistory = () => {
|
||
|
|
const {loadingHistory} = this.state;
|
||
|
|
|
||
|
|
if(loadingHistory) {
|
||
|
|
return (
|
||
|
|
<div style={{fontStyle:'italic', fontSize:11, textAlign: 'center'}} >Loading ...</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// default
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
render() {
|
||
|
|
|
||
|
|
const { network_connection, user_info, chat_messages} = this.props;
|
||
|
|
const admin_info = getAdminInfo();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Fragment >
|
||
|
|
<div
|
||
|
|
style={{height: 600, overflow: 'auto'}}
|
||
|
|
onScroll={this.handleChatboxScroll}
|
||
|
|
// onClick={listenForInChatAction}
|
||
|
|
ref={this.scrollToHistoryDiv}
|
||
|
|
className={'message-list'}
|
||
|
|
>
|
||
|
|
|
||
|
|
<div className="message-list-container">
|
||
|
|
|
||
|
|
{ this.showLoadingHistory() }
|
||
|
|
|
||
|
|
{
|
||
|
|
chat_messages.map((message, index) => <MessageItem key={message.time + '-' + index} is_me={admin_info.id === message.from} {...message} />)
|
||
|
|
}
|
||
|
|
|
||
|
|
{ user_info.typing && <TypingNotification /> }
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{ this.showNotification() }
|
||
|
|
|
||
|
|
<div ref={this.scrollBottomDiv} />
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<InputMessage
|
||
|
|
sendMessage={this.sendMessage}
|
||
|
|
network_connection={network_connection}
|
||
|
|
user_info={user_info}
|
||
|
|
/>
|
||
|
|
</Fragment>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const mapStateToProps = (state: AppState, ownProps: {user_info: UserInfo}) => ({
|
||
|
|
network_connection: state.network_connection,
|
||
|
|
chat_messages: state.current_messages[ownProps.user_info.id] || [],
|
||
|
|
});
|
||
|
|
export default connect(mapStateToProps)(Chatbox);
|