From bdec28c541a4b134614e38621a5cb04f05656a66 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 30 Apr 2020 11:35:03 +1000 Subject: [PATCH] chat-js: load all unreads and autoread on activity if the number of unread messages is larger that the number we are going to load, then load enough messages to display the unread marker. Additionally, only automatically read a message if the user has been active in the last minute. Freeze scroll position on inactivity. Also unconditionally scroll to the bottom upon sending your own message. --- pkg/interface/chat/src/js/components/chat.js | 165 +++++++++++------- .../chat/src/js/components/lib/chat-input.js | 2 + 2 files changed, 108 insertions(+), 59 deletions(-) diff --git a/pkg/interface/chat/src/js/components/chat.js b/pkg/interface/chat/src/js/components/chat.js index 04f34432a7..9f5bf8a77d 100644 --- a/pkg/interface/chat/src/js/components/chat.js +++ b/pkg/interface/chat/src/js/components/chat.js @@ -22,6 +22,36 @@ function getNumPending(props) { return result; } +const ACTIVITY_TIMEOUT = 60000; // a minute + +function scrollIsAtTop(container) { + if ((navigator.userAgent.includes("Safari") && + navigator.userAgent.includes("Chrome")) || + navigator.userAgent.includes("Firefox") + ) { + return container.scrollTop === 0; + } else if (navigator.userAgent.includes("Safari")) { + return container.scrollHeight + Math.round(container.scrollTop) <= + container.clientHeight + 10; + } else { + return false; + } +} + +function scrollIsAtBottom(container) { + if ((navigator.userAgent.includes("Safari") && + navigator.userAgent.includes("Chrome")) || + navigator.userAgent.includes("Firefox") + ) { + return container.scrollHeight - Math.round(container.scrollTop) <= + container.clientHeight + 10; + } else if (navigator.userAgent.includes("Safari")) { + return container.scrollTop === 0; + } else { + return false; + } +} + export class ChatScreen extends Component { constructor(props) { super(props); @@ -30,6 +60,7 @@ export class ChatScreen extends Component { numPages: 1, scrollLocked: false, read: props.read, + active: true, // only for FF lastScrollHeight: null, }; @@ -44,6 +75,10 @@ export class ChatScreen extends Component { this.scrolledToMarker = false; this.setUnreadMarker = this.setUnreadMarker.bind(this); + this.activityTimeout = true; + this.handleActivity = this.handleActivity.bind(this); + this.setInactive = this.setInactive.bind(this); + moment.updateLocale('en', { calendar: { sameDay: '[Today]', @@ -60,8 +95,39 @@ export class ChatScreen extends Component { componentDidMount() { this.askForMessages(); this.scrollToBottom(); + + document.addEventListener("mousemove", this.handleActivity, false); + document.addEventListener("mousedown", this.handleActivity, false); + document.addEventListener("keypress", this.handleActivity, false); + document.addEventListener("touchmove", this.handleActivity, false); + this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); } + componentWillUnmount() { + document.removeEventListener("mousemove", this.handleActivity, false); + document.removeEventListener("mousedown", this.handleActivity, false); + document.removeEventListener("keypress", this.handleActivity, false); + document.removeEventListener("touchmove", this.handleActivity, false); + if(this.activityTimeout) { + clearTimeout(this.activityTimeout); + } + } + + handleActivity() { + if(!this.state.active) { + this.setState({ active: true }); + } + + if(this.activityTimeout) { + clearTimeout(this.activityTimeout); + this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); + } + } + + setInactive() { + this.activityTimeout = null; + this.setState({ active: false, scrollLocked: true }); + } componentDidUpdate(prevProps, prevState) { const { props, state } = this; @@ -97,7 +163,8 @@ export class ChatScreen extends Component { ) { this.hasAskedForMessages = false; } else if(props.length !== prevProps.length && - prevProps.length === prevProps.read + prevProps.length === prevProps.read && + state.active ) { this.setState({ read: props.length }) this.props.api.chat.read(this.props.station); @@ -143,8 +210,16 @@ export class ChatScreen extends Component { let start = props.length - props.envelopes[props.envelopes.length - 1].number; if (start > 0) { - let end = start + 300 < props.length ? start + 300 : props.length; + const unread = props.length - props.read; + const unloadedUnread = unread - props.envelopes.length; + const willLoadUnread = unloadedUnread > 280; + const offset = willLoadUnread ? unloadedUnread + 20 : 300; + const end = start + offset < props.length ? start + offset : props.length; this.hasAskedForMessages = true; + if(willLoadUnread) { + // ensure unread marker is visible + this.setState({ numPages: Math.ceil(unread / 100) }); + } props.subscription.fetchMessages(start + 1, end, props.station); } } @@ -172,64 +247,28 @@ export class ChatScreen extends Component { } onScroll(e) { - if ( - (navigator.userAgent.includes("Safari") && - navigator.userAgent.includes("Chrome")) || - navigator.userAgent.includes("Firefox") - ) { - // Google Chrome and Firefox - if (e.target.scrollTop === 0) { - - // Save scroll position for FF - if (navigator.userAgent.includes('Firefox')) { - - this.setState({ - lastScrollHeight: e.target.scrollHeight - }) + if(scrollIsAtTop(e.target)) { + // Save scroll position for FF + if (navigator.userAgent.includes('Firefox')) { + this.setState({ + lastScrollHeight: e.target.scrollHeight + }); + } + this.setState( + { + numPages: this.state.numPages + 1, + scrollLocked: true + }, + () => { + this.askForMessages(); } - this.setState( - { - numPages: this.state.numPages + 1, - scrollLocked: true - }, - () => { - this.askForMessages(); - } - ); - } else if ( - e.target.scrollHeight - Math.round(e.target.scrollTop) === - e.target.clientHeight - ) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false, - }); - } - } else if (navigator.userAgent.includes("Safari")) { - // Safari - if (e.target.scrollTop === 0) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false - }); - } else if ( - e.target.scrollHeight + Math.round(e.target.scrollTop) <= - e.target.clientHeight + 10 - ) { - this.setState( - { - numPages: this.state.numPages + 1, - scrollLocked: true - }, - () => { - this.askForMessages(); - } - ); - } - } else { - console.log("Your browser is not supported."); + ); + } else if (scrollIsAtBottom(e.target)) { + this.dismissUnread(); + this.setState({ + numPages: 1, + scrollLocked: false + }); } } @@ -237,6 +276,13 @@ export class ChatScreen extends Component { if(ref && !this.scrolledToMarker) { this.setState({ scrollLocked: true }, () => { ref.scrollIntoView({ block: 'center' }); + if(scrollIsAtBottom(ref.offsetParent)) { + this.dismissUnread(); + this.setState({ + numPages: 1, + scrollLocked: false + }); + } }); this.scrolledToMarker = true; } @@ -478,6 +524,7 @@ export class ChatScreen extends Component { ownerContact={ownerContact} envelopes={props.envelopes} contacts={props.contacts} + onEnter={() => this.setState({ scrollLocked: false })} placeholder="Message..." /> diff --git a/pkg/interface/chat/src/js/components/lib/chat-input.js b/pkg/interface/chat/src/js/components/lib/chat-input.js index 6a61b57f19..ae94ca96f3 100644 --- a/pkg/interface/chat/src/js/components/lib/chat-input.js +++ b/pkg/interface/chat/src/js/components/lib/chat-input.js @@ -192,6 +192,8 @@ export class ChatInput extends Component { return; } + props.onEnter(); + if(state.code) { props.api.chat.message(props.station, `~${window.ship}`, Date.now(), { code: {