Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(webchat): inconsistent scrolling behavior #550

Merged
merged 2 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/components/src/css/botpress-default.css
Original file line number Diff line number Diff line change
Expand Up @@ -258,16 +258,23 @@ body {
min-height: 1px;
}

.bpw-msg-list {
.bpw-msg-list-scroll-container {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
overflow-y: auto;
overflow-y: hidden;
-ms-flex-positive: 1;
flex-grow: 1;
}

.bpw-msg-list {
padding: 0 0.5rem 0.5rem 0.5rem;
}

.bpw-msg-list-follow {
display: none;
}

.bpw-date-container {
color: #666;
text-align: center;
Expand Down
1 change: 1 addition & 0 deletions packages/webchat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"react-dom": "^17.0.2",
"react-ga": "^2.7.0",
"react-intl": "^3.12.1",
"react-scroll-to-bottom": "^4.2.0",
"snarkdown": "^2.0.0",
"uuid": "^8.3.2"
}
Expand Down
160 changes: 63 additions & 97 deletions packages/webchat/src/components/messages/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ResizeObserver } from '@juggle/resize-observer'
import differenceInMinutes from 'date-fns/difference_in_minutes'
import debounce from 'lodash/debounce'
import last from 'lodash/last'
import { observe } from 'mobx'
import { inject, observer } from 'mobx-react'
import React from 'react'
import React, { useEffect, useState } from 'react'
import { WrappedComponentProps, injectIntl } from 'react-intl'
import ScrollToBottom, { useScrollToBottom, useSticky } from 'react-scroll-to-bottom'

import constants from '../../core/constants'
import { RootStore, StoreDef } from '../../store'
Expand All @@ -14,76 +14,63 @@ import Avatar from '../common/Avatar'
import MessageGroup from './MessageGroup'

interface State {
manualScroll: boolean
showNewMessageIndicator: boolean
messagesLength: number | undefined
}

class MessageList extends React.Component<MessageListProps, State> {
private messagesDiv!: HTMLElement
private divSizeObserver!: ResizeObserver
state: State = { showNewMessageIndicator: false, manualScroll: false }

componentDidMount() {
this.tryScrollToBottom(true)

observe(this.props.focusedArea!, (focus) => {
focus.newValue === 'convo' && this.messagesDiv.focus()
})

if (this.props.currentMessages) {
observe(this.props.currentMessages, (messages) => {
if (this.state.manualScroll) {
if (!this.state.showNewMessageIndicator) {
this.setState({ showNewMessageIndicator: true })
}
return
}
this.tryScrollToBottom()
})
}

// this should account for keyboard rendering as it triggers a resize of the messagesDiv
this.divSizeObserver = new ResizeObserver(
debounce(
(_divResizeEntry) => {
// we don't need to do anything with the resize entry
this.tryScrollToBottom()
},
200,
{ trailing: true }
)
)
this.divSizeObserver.observe(this.messagesDiv)
}

componentWillUnmount() {
this.divSizeObserver.disconnect()
render() {
return (
<ScrollToBottom
mode={'bottom'}
initialScrollBehavior={'auto'}
tabIndex={0}
className={'bpw-msg-list-scroll-container'}
scrollViewClassName={'bpw-msg-list'}
ref={(m: any) => {
this.messagesDiv = m
}}
followButtonClassName={'bpw-msg-list-follow'}
>
<Content {...this.props} />
</ScrollToBottom>
)
}
}

componentDidUpdate() {
if (this.state.manualScroll) {
return
const Content = observer((props: MessageListProps) => {
const [state, setState] = useState<State>({
showNewMessageIndicator: false,
messagesLength: undefined
})
const scrollToBottom = useScrollToBottom()
const [sticky] = useSticky()

useEffect(() => {
const stateUpdate = { ...state, messagesLength: props?.currentMessages?.length }
if (!sticky && state.messagesLength !== props?.currentMessages?.length) {
setState({ ...stateUpdate, showNewMessageIndicator: true })
} else {
setState({ ...stateUpdate, showNewMessageIndicator: false })
}
this.tryScrollToBottom()
}
}, [props?.currentMessages?.length, sticky])

tryScrollToBottom(delayed?: boolean) {
setTimeout(
() => {
try {
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight
} catch (err) {
// Discard the error
}
},
delayed ? 250 : 0
)
const shouldDisplayMessage = (m: Message): boolean => {
return m.payload.type !== 'postback'
}

renderDate(date: Date) {
const renderDate = (date: Date) => {
return (
<div className={'bpw-date-container'}>
{new Intl.DateTimeFormat(this.props.intl.locale || 'en', {
{new Intl.DateTimeFormat(props.intl.locale || 'en', {
month: 'short',
day: 'numeric',
hour: 'numeric',
Expand All @@ -94,20 +81,20 @@ class MessageList extends React.Component<MessageListProps, State> {
)
}

renderAvatar(name: string, url: string) {
const renderAvatar = (name: string, url: string) => {
const avatarSize = 40
return <Avatar name={name} avatarUrl={url} height={avatarSize} width={avatarSize} />
}

renderMessageGroups() {
const messages = (this.props.currentMessages || []).filter((m) => this.shouldDisplayMessage(m))
const renderMessageGroups = () => {
const messages = (props.currentMessages || []).filter((m: any) => shouldDisplayMessage(m))
const groups: Message[][] = []

let lastSpeaker: string | undefined = undefined
let lastDate: Date | undefined = undefined
let currentGroup: Message[] | undefined = undefined

messages.forEach((m) => {
messages.forEach((m: any) => {
const speaker = m.authorId
const date = m.sentOn

Expand All @@ -130,7 +117,7 @@ class MessageList extends React.Component<MessageListProps, State> {
lastDate = date
})

if (this.props.isBotTyping!.get()) {
if (props?.isBotTyping?.get()) {
if (lastSpeaker !== 'bot') {
currentGroup = []
groups.push(currentGroup)
Expand All @@ -152,13 +139,13 @@ class MessageList extends React.Component<MessageListProps, State> {
!groups[i - 1] ||
differenceInMinutes(new Date(groupDate), new Date(lastDate)) > constants.TIME_BETWEEN_DATES

const [{ authorId }] = group
const { authorId } = last(group) as Message

const avatar = !authorId && this.renderAvatar(this.props.botName!, this.props.botAvatarUrl!)
const avatar = !authorId && renderAvatar(props.botName!, props.botAvatarUrl!)

return (
<div key={i}>
{isDateNeeded && this.renderDate(group[0].sentOn)}
{isDateNeeded && renderDate(group[0].sentOn)}
<MessageGroup
isBot={!authorId}
avatar={avatar as JSX.Element}
Expand All @@ -173,42 +160,21 @@ class MessageList extends React.Component<MessageListProps, State> {
)
}

shouldDisplayMessage = (m: Message): boolean => {
return m.payload.type !== 'postback'
}

handleScroll = debounce((e) => {
const scroll = this.messagesDiv.scrollHeight - this.messagesDiv.scrollTop - this.messagesDiv.clientHeight
const manualScroll = scroll >= 150
const showNewMessageIndicator = this.state.showNewMessageIndicator && manualScroll

this.setState({ manualScroll, showNewMessageIndicator })
}, 50)

render() {
return (
<div
tabIndex={0}
className={'bpw-msg-list'}
ref={(m) => {
this.messagesDiv = m!
}}
onScroll={this.handleScroll}
>
{this.state.showNewMessageIndicator && (
<div className="bpw-new-messages-indicator" onClick={(e) => this.tryScrollToBottom()}>
<span>
{this.props.intl.formatMessage({
id: `messages.newMessage${this.props.currentMessages!.length === 1 ? '' : 's'}`
})}
</span>
</div>
)}
{this.renderMessageGroups()}
</div>
)
}
}
return (
<>
{state.showNewMessageIndicator && (
<div className="bpw-new-messages-indicator" onClick={(e) => scrollToBottom()}>
<span>
{props.intl.formatMessage({
id: `messages.newMessage${props?.currentMessages?.length === 1 ? '' : 's'}`
})}
</span>
</div>
)}
{renderMessageGroups()}
</>
)
})

export default inject(({ store }: { store: RootStore }) => ({
intl: store.intl,
Expand Down
1 change: 1 addition & 0 deletions packages/webchat/src/declaration.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
declare module '*.scss'
declare module 'react-scroll-to-bottom'