@ -6,7 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import chatService from '../../services/chat_service/chat_service.js'
import chatService from '../../services/chat_service/chat_service.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition , getNewTopPosition , isBottomedOut , scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
import { getScrollPosition , getNewTopPosition , isBottomedOut , isScrollable } from './chat_layout_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
import {
faChevronDown ,
faChevronDown ,
@ -20,7 +20,7 @@ library.add(
)
)
const BOTTOMED _OUT _OFFSET = 10
const BOTTOMED _OUT _OFFSET = 10
const JUMP _TO _BOTTOM _BUTTON _VISIBILITY _OFFSET = 1 5 0
const JUMP _TO _BOTTOM _BUTTON _VISIBILITY _OFFSET = 1 0
const SAFE _RESIZE _TIME _OFFSET = 100
const SAFE _RESIZE _TIME _OFFSET = 100
const MARK _AS _READ _DELAY = 1500
const MARK _AS _READ _DELAY = 1500
const MAX _RETRIES = 10
const MAX _RETRIES = 10
@ -43,7 +43,7 @@ const Chat = {
} ,
} ,
created ( ) {
created ( ) {
this . startFetching ( )
this . startFetching ( )
window . addEventListener ( 'resize' , this . handle LayoutChang e)
window . addEventListener ( 'resize' , this . handle Resiz e)
} ,
} ,
mounted ( ) {
mounted ( ) {
window . addEventListener ( 'scroll' , this . handleScroll )
window . addEventListener ( 'scroll' , this . handleScroll )
@ -52,15 +52,11 @@ const Chat = {
}
}
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
this . updateScrollableContainerHeight ( )
this . handleResize ( )
this . handleResize ( )
} )
} )
this . setChatLayout ( )
} ,
} ,
destroy ed ( ) {
unmount ed ( ) {
window . removeEventListener ( 'scroll' , this . handleScroll )
window . removeEventListener ( 'scroll' , this . handleScroll )
window . removeEventListener ( 'resize' , this . handleLayoutChange )
this . unsetChatLayout ( )
if ( typeof document . hidden !== 'undefined' ) document . removeEventListener ( 'visibilitychange' , this . handleVisibilityChange , false )
if ( typeof document . hidden !== 'undefined' ) document . removeEventListener ( 'visibilitychange' , this . handleVisibilityChange , false )
this . $store . dispatch ( 'clearCurrentChat' )
this . $store . dispatch ( 'clearCurrentChat' )
} ,
} ,
@ -96,8 +92,7 @@ const Chat = {
... mapState ( {
... mapState ( {
backendInteractor : state => state . api . backendInteractor ,
backendInteractor : state => state . api . backendInteractor ,
mastoUserSocketStatus : state => state . api . mastoUserSocketStatus ,
mastoUserSocketStatus : state => state . api . mastoUserSocketStatus ,
mobileLayout : state => state . interface . mobileLayout ,
mobileLayout : state => state . interface . layoutType === 'mobile' ,
layoutHeight : state => state . interface . layoutHeight ,
currentUser : state => state . users . currentUser
currentUser : state => state . users . currentUser
} )
} )
} ,
} ,
@ -115,9 +110,6 @@ const Chat = {
'$route' : function ( ) {
'$route' : function ( ) {
this . startFetching ( )
this . startFetching ( )
} ,
} ,
layoutHeight ( ) {
this . handleResize ( { expand : true } )
} ,
mastoUserSocketStatus ( newValue ) {
mastoUserSocketStatus ( newValue ) {
if ( newValue === WSConnectionStatus . JOINED ) {
if ( newValue === WSConnectionStatus . JOINED ) {
this . fetchChat ( { isFirstFetch : true } )
this . fetchChat ( { isFirstFetch : true } )
@ -132,7 +124,6 @@ const Chat = {
onFilesDropped ( ) {
onFilesDropped ( ) {
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
this . handleResize ( )
this . handleResize ( )
this . updateScrollableContainerHeight ( )
} )
} )
} ,
} ,
handleVisibilityChange ( ) {
handleVisibilityChange ( ) {
@ -142,43 +133,7 @@ const Chat = {
}
}
} )
} )
} ,
} ,
setChatLayout ( ) {
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
// This is a hacky way to adjust the global layout to the mobile chat (without modifying the rest of the app).
// This layout prevents empty spaces from being visible at the bottom
// of the chat on iOS Safari (`safe-area-inset`) when
// - the on-screen keyboard appears and the user starts typing
// - the user selects the text inside the input area
// - the user selects and deletes the text that is multiple lines long
// TODO: unify the chat layout with the global layout.
let html = document . querySelector ( 'html' )
if ( html ) {
html . classList . add ( 'chat-layout' )
}
this . $nextTick ( ( ) => {
this . updateScrollableContainerHeight ( )
} )
} ,
unsetChatLayout ( ) {
let html = document . querySelector ( 'html' )
if ( html ) {
html . classList . remove ( 'chat-layout' )
}
} ,
handleLayoutChange ( ) {
this . $nextTick ( ( ) => {
this . updateScrollableContainerHeight ( )
this . scrollDown ( )
} )
} ,
// Ensures the proper position of the posting form in the mobile layout (the mobile browser panel does not overlap or hide it)
updateScrollableContainerHeight ( ) {
const header = this . $refs . header
const footer = this . $refs . footer
const inner = this . mobileLayout ? window . document . body : this . $refs . inner
this . scrollableContainerHeight = scrollableContainerHeight ( inner , header , footer ) + 'px'
} ,
// Preserves the scroll position when OSK appears or the posting form changes its height.
handleResize ( opts = { } ) {
handleResize ( opts = { } ) {
const { expand = false , delayed = false } = opts
const { expand = false , delayed = false } = opts
@ -190,29 +145,20 @@ const Chat = {
}
}
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
this . updateScrollableContainerHeight ( )
const { offsetHeight = undefined } = getScrollPosition ( )
const { offsetHeight = undefined } = this . lastScrollPosition
this . lastScrollPosition = getScrollPosition ( this . $refs . scrollable )
const diff = this . lastScrollPosition . offsetHeight - offsetHeight
const diff = this . lastScrollPosition . offsetHeight - offsetHeight
if ( diff < 0 || ( ! this . bottomedOut ( ) && expand ) ) {
if ( diff !== 0 || ( ! this . bottomedOut ( ) && expand ) ) {
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
this . updateScrollableContainerHeight ( )
window . scrollTo ( { top : window . scrollY + diff } )
this . $refs . scrollable . scrollTo ( {
top : this . $refs . scrollable . scrollTop - diff ,
left : 0
} )
} )
} )
}
}
this . lastScrollPosition = getScrollPosition ( )
} )
} )
} ,
} ,
scrollDown ( options = { } ) {
scrollDown ( options = { } ) {
const { behavior = 'auto' , forceRead = false } = options
const { behavior = 'auto' , forceRead = false } = options
const scrollable = this . $refs . scrollable
if ( ! scrollable ) { return }
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
scrollable . scrollTo ( { top : scrollable . scrollHeight , left : 0 , behavior } )
window . scrollTo ( { top : document . documentElement . scrollHeight , behavior } )
} )
} )
if ( forceRead ) {
if ( forceRead ) {
this . readChat ( )
this . readChat ( )
@ -228,11 +174,10 @@ const Chat = {
} )
} )
} ,
} ,
bottomedOut ( offset ) {
bottomedOut ( offset ) {
return isBottomedOut ( this . $refs . scrollable , offset )
return isBottomedOut ( offset )
} ,
} ,
reachedTop ( ) {
reachedTop ( ) {
const scrollable = this . $refs . scrollable
return window . scrollY <= 0
return scrollable && scrollable . scrollTop <= 0
} ,
} ,
cullOlderCheck ( ) {
cullOlderCheck ( ) {
window . setTimeout ( ( ) => {
window . setTimeout ( ( ) => {
@ -263,10 +208,9 @@ const Chat = {
}
}
} , 200 ) ,
} , 200 ) ,
handleScrollUp ( positionBeforeLoading ) {
handleScrollUp ( positionBeforeLoading ) {
const positionAfterLoading = getScrollPosition ( this . $refs . scrollable )
const positionAfterLoading = getScrollPosition ( )
this . $refs . scrollable . scrollTo ( {
window . scrollTo ( {
top : getNewTopPosition ( positionBeforeLoading , positionAfterLoading ) ,
top : getNewTopPosition ( positionBeforeLoading , positionAfterLoading )
left : 0
} )
} )
} ,
} ,
fetchChat ( { isFirstFetch = false , fetchLatest = false , maxId } ) {
fetchChat ( { isFirstFetch = false , fetchLatest = false , maxId } ) {
@ -285,22 +229,18 @@ const Chat = {
chatService . clear ( chatMessageService )
chatService . clear ( chatMessageService )
}
}
const positionBeforeUpdate = getScrollPosition ( this . $refs . scrollable )
const positionBeforeUpdate = getScrollPosition ( )
this . $store . dispatch ( 'addChatMessages' , { chatId , messages } ) . then ( ( ) => {
this . $store . dispatch ( 'addChatMessages' , { chatId , messages } ) . then ( ( ) => {
this . $nextTick ( ( ) => {
this . $nextTick ( ( ) => {
if ( fetchOlderMessages ) {
if ( fetchOlderMessages ) {
this . handleScrollUp ( positionBeforeUpdate )
this . handleScrollUp ( positionBeforeUpdate )
}
}
if ( isFirstFetch ) {
this . updateScrollableContainerHeight ( )
}
// In vertical screens, the first batch of fetched messages may not always take the
// In vertical screens, the first batch of fetched messages may not always take the
// full height of the scrollable container.
// full height of the scrollable container.
// If this is the case, we want to fetch the messages until the scrollable container
// If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history.
// is fully populated so that the user has the ability to scroll up and load the history.
if ( ! isScrollable ( this . $refs . scrollable ) && messages . length > 0 ) {
if ( ! isScrollable ( ) && messages . length > 0 ) {
this . fetchChat ( { maxId : this . currentChatMessageService . minId } )
this . fetchChat ( { maxId : this . currentChatMessageService . minId } )
}
}
} )
} )
@ -336,9 +276,6 @@ const Chat = {
this . handleResize ( )
this . handleResize ( )
// When the posting form size changes because of a media attachment, we need an extra resize
// When the posting form size changes because of a media attachment, we need an extra resize
// to account for the potential delay in the DOM update.
// to account for the potential delay in the DOM update.
setTimeout ( ( ) => {
this . updateScrollableContainerHeight ( )
} , SAFE _RESIZE _TIME _OFFSET )
this . scrollDown ( { forceRead : true } )
this . scrollDown ( { forceRead : true } )
} )
} )
} ,
} ,