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 { 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; private scrollToHistoryDiv: React.RefObject; constructor(props: ChatboxProps) { super(props); this.state = { loadingHistory: false } this.scrollBottomDiv = createRef(); this.scrollToHistoryDiv = createRef(); 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) => (
{txt}
); 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 (
{ list_messages.map((txt, index) => NotiMessage(txt, index)) }
) } // 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 (
Loading ...
) } // default return null; } render() { const { network_connection, user_info, chat_messages} = this.props; const admin_info = getAdminInfo(); return (
{ this.showLoadingHistory() } { chat_messages.map((message, index) => ) } { user_info.typing && }
{ this.showNotification() }
) } } 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);