Files
chatngay_chatboard_dev/src/components/Chatbox/ver/Chatbox.tsx

312 lines
10 KiB
TypeScript
Raw Normal View History

2021-05-19 11:57:16 +07:00
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);