Skip to content

Commit

Permalink
fix(channel-web): inconsistent scrolling behavior (#12193)
Browse files Browse the repository at this point in the history
* fix(channel-web): inconsistent scrolling behavior

* update css on HITL-NEXT
  • Loading branch information
davidvitora committed Oct 24, 2022
1 parent d9307ee commit c2f158c
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 157 deletions.
13 changes: 10 additions & 3 deletions modules/channel-web/assets/default-emulator.css
Expand Up @@ -370,14 +370,21 @@
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;
padding: 0 var(--spacing-large);
}

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

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

.bpw-date-container {
Expand Down
11 changes: 9 additions & 2 deletions modules/channel-web/assets/default.css
Expand Up @@ -278,16 +278,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
3 changes: 2 additions & 1 deletion modules/channel-web/package.json
Expand Up @@ -41,7 +41,7 @@
"lru-cache": "^6.0.0",
"mime": "^2.5.2",
"mobx": "4.9.4",
"mobx-react": "^5.4.4",
"mobx-react": "6.0.1",
"moment": "^2.24.0",
"ms": "^2.1.3",
"multer": "^1.4.2",
Expand All @@ -50,6 +50,7 @@
"react-ga": "^2.7.0",
"react-intl": "^2.8.0",
"react-linkify": "^0.2.2",
"react-scroll-to-bottom": "^4.2.0",
"react-select": "^2.4.2",
"react-slick": "^0.24.0",
"slick-carousel": "^1.8.1",
Expand Down
4 changes: 2 additions & 2 deletions modules/channel-web/src/views/lite/components/Composer.tsx
Expand Up @@ -23,7 +23,7 @@ class Composer extends React.Component<ComposerProps, { isRecording: boolean }>
this.focus()

observe(this.props.focusedArea, focus => {
focus.newValue === 'input' && this.textInput.current.focus()
focus.newValue === 'input' && this.textInput.current?.focus()
})
}

Expand All @@ -36,7 +36,7 @@ class Composer extends React.Component<ComposerProps, { isRecording: boolean }>

focus = () => {
setTimeout(() => {
this.textInput.current.focus()
this.textInput.current?.focus()
}, 50)
}

Expand Down
180 changes: 72 additions & 108 deletions modules/channel-web/src/views/lite/components/messages/MessageList.tsx
@@ -1,11 +1,10 @@
import { ResizeObserver } from '@juggle/resize-observer'
import differenceInMinutes from 'date-fns/difference_in_minutes'
import _ from 'lodash'
import debounce from 'lodash/debounce'
import { observe } from 'mobx'
import { inject, observer } from 'mobx-react'
import React from 'react'
import React, { useEffect, useState } from 'react'
import { InjectedIntlProps, 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 @@ -15,69 +14,17 @@ import Avatar from '../common/Avatar'
import MessageGroup from './MessageGroup'

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

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()
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]) => {
this.tryScrollToBottom()
},
200,
{ trailing: true }
)
)
this.divSizeObserver.observe(this.messagesDiv)
}

componentWillUnmount() {
this.divSizeObserver.disconnect()
}

componentDidUpdate() {
if (this.state.manualScroll) {
return
}
this.tryScrollToBottom()
}

tryScrollToBottom(delayed?: boolean) {
setTimeout(
() => {
try {
this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight
} catch (err) {
// Discard the error
}
},
delayed ? 250 : 0
)
}

handleKeyDown = e => {
Expand All @@ -101,10 +48,55 @@ class MessageList extends React.Component<MessageListProps, State> {
}
}

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

const Content = observer(props => {
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 })
}
}, [props.currentMessages.length, sticky])

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

const renderAvatar = (name, url) => {
const avatarSize = props.isEmulator ? 20 : 40 // quick fix
return <Avatar name={name} avatarUrl={url} height={avatarSize} width={avatarSize} />
}

const renderDate = 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 @@ -115,13 +107,8 @@ class MessageList extends React.Component<MessageListProps, State> {
)
}

renderAvatar(name, url) {
const avatarSize = this.props.isEmulator ? 20 : 40 // quick fix
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 => shouldDisplayMessage(m))
const groups: Message[][] = []

let lastSpeaker = undefined
Expand Down Expand Up @@ -151,7 +138,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 @@ -177,13 +164,12 @@ class MessageList extends React.Component<MessageListProps, State> {
const { authorId, payload } = _.last(group)

const avatar = authorId
? this.props.showUserAvatar &&
this.renderAvatar(payload.channel?.web?.userName, payload.channel?.web?.avatarUrl)
: this.renderAvatar(this.props.botName, payload.channel?.web?.avatarUrl || this.props.botAvatarUrl)
? props.showUserAvatar && renderAvatar(payload.channel?.web?.userName, payload.channel?.web?.avatarUrl)
: renderAvatar(props.botName, payload.channel?.web?.avatarUrl || props.botAvatarUrl)

return (
<div key={i}>
{isDateNeeded && this.renderDate(group[0].sentOn)}
{isDateNeeded && renderDate(group[0].sentOn)}
<MessageGroup
isBot={!authorId}
avatar={avatar}
Expand All @@ -199,43 +185,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 > 0
const showNewMessageIndicator = this.state.showNewMessageIndicator && manualScroll

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

render() {
return (
<div
tabIndex={0}
onKeyDown={this.handleKeyDown}
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
29 changes: 27 additions & 2 deletions modules/channel-web/src/views/lite/translations/pt.json
@@ -1,11 +1,18 @@
{
"composer.placeholder": "Fale com {name}",
"composer.placeholderInit": "Diga alguma coisa para {name}",
"composer.placeholderInit": "Diga algo para {name}",
"composer.send": "Mandar",
"composer.message": "Mensagem para enviar",
"composer.sendMessage": "Enviar Mensagem",
"header.conversations": "Conversas",
"header.deleteConversation": "Tem certeza de que deseja excluir toda esta conversa?",
"header.deleteConversationYes": "Excluir",
"header.deleteConversationNo": "Cancelar",
"header.botInfo": "Informação do Bot",
"header.downloadConversation": "Baixar conversa",
"header.hideChatWindow": "Esconder a janela de chat",
"header.deleteConversationButton": "Excluir conversa",
"header.resetConversation": "Resetar conversa",
"conversationList.untitledConversation": "Conversa sem título",
"conversationList.title": "Conversa {id}",
"botInfo.backToConversation": "De volta à conversa",
Expand All @@ -15,5 +22,23 @@
"botInfo.preferredLanguage": "Preferred Language",
"store.resetSessionMessage": "Redefinir a conversa",
"messages.newMessage": "Nova mensagem",
"messages.newMessages": "Novas mensagens"
"messages.newMessages": "Novas mensagens",
"message.botSaid": "Assistente Virtual disse : ",
"message.fromBotLabel": "Assistente Virtual",
"message.fromMeLabel": "Eu",
"message.iSaid": "Eu disse : ",
"messages.readMore": "Leia Mais",
"messages.showLess": "Mostrar Menos",
"message.thumbsUp": "Positivo",
"message.thumbsDown": "Negativo",
"widget.title": "Janela de chat",
"widget.toggle": "Alterar janela do chatbot",
"composer.dropdownPlaceholder": "Selecione uma opção",
"composer.interact": "Interaja com o chatbot",
"footer.poweredBy": "Nós somos {icon} por {link}",
"loginForm.userName": "Username",
"loginForm.password": "Senha",
"loginForm.submit": "Enviar",
"loginForm.formTitle": "Formulário de login",
"loginForm.providedCredentials": "Informe as credenciais"
}

0 comments on commit c2f158c

Please sign in to comment.