mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 12:04:56 +02:00
756 lines
23 KiB
Svelte
756 lines
23 KiB
Svelte
<!--
|
|
// Copyright © 2024 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
-->
|
|
<script lang="ts">
|
|
import activity, { ActivityMessage } from '@hcengineering/activity'
|
|
import {
|
|
ActivityMessagePresenter,
|
|
canGroupMessages,
|
|
messageInFocus,
|
|
editingMessageStore
|
|
} from '@hcengineering/activity-resources'
|
|
import core, { Doc, generateId, getCurrentAccount, Ref, Space, Timestamp, Tx, TxCUD } from '@hcengineering/core'
|
|
import { DocNotifyContext } from '@hcengineering/notification'
|
|
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
|
import { addTxListener, getClient, removeTxListener } from '@hcengineering/presentation'
|
|
import { ModernButton, Scroller } from '@hcengineering/ui'
|
|
import { afterUpdate, onDestroy, onMount, tick } from 'svelte'
|
|
import { ChatMessage } from '@hcengineering/chunter'
|
|
|
|
import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider'
|
|
import chunter from '../plugin'
|
|
import { getScrollToDateOffset, getSelectedDate, jumpToDate, messageInView, readViewportMessages } from '../scroll'
|
|
import { chatReadMessagesStore, recheckNotifications } from '../utils'
|
|
import BaseChatScroller from './BaseChatScroller.svelte'
|
|
import BlankView from './BlankView.svelte'
|
|
import ChannelInput from './ChannelInput.svelte'
|
|
import ActivityMessagesSeparator from './ChannelMessagesSeparator.svelte'
|
|
import JumpToDateSelector from './JumpToDateSelector.svelte'
|
|
import HistoryLoading from './LoadingHistory.svelte'
|
|
|
|
export let provider: ChannelDataProvider
|
|
export let object: Doc
|
|
export let channel: Doc
|
|
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
|
export let fixedInput = true
|
|
export let collection: string = 'messages'
|
|
export let fullHeight = true
|
|
export let freeze = false
|
|
export let loadMoreAllowed = true
|
|
export let autofocus = true
|
|
export let withInput: boolean = true
|
|
export let readonly: boolean = false
|
|
export let onReply: ((message: ActivityMessage) => void) | undefined = undefined
|
|
|
|
const minMsgHeightRem = 2
|
|
const loadMoreThreshold = 200
|
|
const newSeparatorOffset = 150
|
|
|
|
const account = getCurrentAccount()
|
|
const socialStrings = account.socialIds
|
|
const client = getClient()
|
|
const hierarchy = client.getHierarchy()
|
|
const inboxClient = InboxNotificationsClientImpl.getClient()
|
|
const contextByDocStore = inboxClient.contextByDoc
|
|
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
|
|
|
|
// Stores
|
|
const metadataStore = provider.metadataStore
|
|
const messagesStore = provider.messagesStore
|
|
const isLoadingStore = provider.isLoadingStore
|
|
const isTailLoadedStore = provider.isTailLoaded
|
|
const newTimestampStore = provider.newTimestampStore
|
|
const datesStore = provider.datesStore
|
|
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
|
const isLoadingMoreStore = provider.isLoadingMoreStore
|
|
|
|
const doc = object
|
|
const uuid = generateId()
|
|
|
|
let messages: ActivityMessage[] = []
|
|
let messagesCount = 0
|
|
|
|
// Elements
|
|
let scroller: Scroller | undefined | null = undefined
|
|
let scrollDiv: HTMLDivElement | undefined | null = undefined
|
|
let contentDiv: HTMLDivElement | undefined | null = undefined
|
|
let separatorDiv: HTMLDivElement | undefined | null = undefined
|
|
|
|
// Dates
|
|
let selectedDate: Timestamp | undefined = undefined
|
|
let dateToJump: Timestamp | undefined = undefined
|
|
|
|
// Scrolling
|
|
let isScrollInitialized = false
|
|
let shouldScrollToNew = false
|
|
let isScrollAtBottom = false
|
|
|
|
let isLatestMessageButtonVisible = false
|
|
|
|
// Pagination
|
|
let backwardRequested = false
|
|
let restoreScrollTop = 0
|
|
let restoreScrollHeight = 0
|
|
|
|
let isPageHidden = false
|
|
let lastMsgBeforeFreeze: Ref<ActivityMessage> | undefined = undefined
|
|
let needUpdateTimestamp = false
|
|
|
|
$: messages = $messagesStore
|
|
$: notifyContext = $contextByDocStore.get(doc._id)
|
|
$: isThread = hierarchy.isDerived(doc._class, activity.class.ActivityMessage)
|
|
$: isChunterSpace = hierarchy.isDerived(doc._class, chunter.class.ChunterSpace)
|
|
$: readonly = hierarchy.isDerived(channel._class, core.class.Space)
|
|
? readonly || (channel as Space).archived
|
|
: readonly
|
|
|
|
$: separatorIndex =
|
|
$newTimestampStore !== undefined
|
|
? messages.findIndex((message) => (message.createdOn ?? 0) >= ($newTimestampStore ?? 0))
|
|
: -1
|
|
|
|
$: if (!freeze && !isPageHidden && isScrollInitialized) {
|
|
read()
|
|
}
|
|
|
|
const unsubscribe = inboxClient.inboxNotificationsByContext.subscribe(() => {
|
|
if (notifyContext !== undefined && !isFreeze()) {
|
|
recheckNotifications(notifyContext)
|
|
read()
|
|
}
|
|
})
|
|
|
|
$: void initializeScroll($isLoadingStore, separatorDiv, separatorIndex)
|
|
$: adjustScrollPosition(selectedMessageId)
|
|
$: void handleMessagesUpdated(messages.length)
|
|
|
|
function adjustScrollPosition (selectedMessageId?: Ref<ActivityMessage>): void {
|
|
if ($isLoadingStore || !isScrollInitialized) {
|
|
return
|
|
}
|
|
const msgData = $metadataStore.find(({ _id }) => _id === selectedMessageId)
|
|
if (msgData !== undefined) {
|
|
const isReload = provider.jumpToMessage(msgData)
|
|
if (isReload) {
|
|
reinitializeScroll()
|
|
} else {
|
|
scrollToMessage()
|
|
}
|
|
} else if (selectedMessageId === undefined) {
|
|
provider.jumpToEnd()
|
|
reinitializeScroll()
|
|
}
|
|
}
|
|
|
|
function handleWindowFocus (): void {
|
|
checkWindowVisibility(false)
|
|
}
|
|
|
|
function handleWindowBlur (): void {
|
|
checkWindowVisibility(true)
|
|
}
|
|
|
|
function handleVisibilityChange (): void {
|
|
checkWindowVisibility(document.hidden)
|
|
}
|
|
|
|
function checkWindowVisibility (hidden: boolean): void {
|
|
if (document.hidden || !document.hasFocus() || hidden) {
|
|
if (isPageHidden) return
|
|
isPageHidden = true
|
|
needUpdateTimestamp = true
|
|
lastMsgBeforeFreeze = shouldScrollToNew ? messages[messages.length - 1]?._id : undefined
|
|
} else {
|
|
if (isPageHidden) {
|
|
isPageHidden = false
|
|
needUpdateTimestamp = false
|
|
}
|
|
}
|
|
}
|
|
|
|
function isFreeze (): boolean {
|
|
return freeze || isPageHidden
|
|
}
|
|
|
|
function scrollToBottom (): void {
|
|
if (scroller != null && scrollDiv != null && !isFreeze()) {
|
|
scrollDiv.scroll({ top: 0, behavior: 'instant' })
|
|
updateSelectedDate()
|
|
}
|
|
}
|
|
|
|
function scrollToSeparator (): void {
|
|
if (separatorDiv == null || scrollDiv == null || contentDiv == null) {
|
|
return
|
|
}
|
|
|
|
const messagesElements = contentDiv?.getElementsByClassName('activityMessage')
|
|
const messagesHeight = messages
|
|
.slice(separatorIndex)
|
|
.reduce((res, msg) => res + (messagesElements?.[msg._id as any]?.clientHeight ?? 0), 0)
|
|
|
|
separatorDiv.scrollIntoView()
|
|
|
|
if (messagesHeight >= scrollDiv.clientHeight) {
|
|
scroller?.scrollBy(-newSeparatorOffset)
|
|
}
|
|
|
|
updateShouldScrollToNew()
|
|
read()
|
|
}
|
|
|
|
function scrollToMessage (): void {
|
|
if (selectedMessageId === undefined) return
|
|
if (scrollDiv == null || contentDiv == null) {
|
|
setTimeout(scrollToMessage, 50)
|
|
return
|
|
}
|
|
|
|
const messagesElements = contentDiv?.getElementsByClassName('activityMessage')
|
|
const msgElement = messagesElements?.[selectedMessageId as any]
|
|
|
|
if (msgElement == null) {
|
|
if (messages.some(({ _id }) => _id === selectedMessageId)) {
|
|
setTimeout(scrollToMessage, 50)
|
|
return
|
|
}
|
|
} else {
|
|
msgElement.scrollIntoView({ block: 'start' })
|
|
}
|
|
read()
|
|
}
|
|
|
|
function scrollToStartOfNew (): void {
|
|
if (scrollDiv == null || lastMsgBeforeFreeze === undefined) return
|
|
if (needUpdateTimestamp || $newTimestampStore === undefined) {
|
|
void provider.updateNewTimestamp(notifyContext)
|
|
needUpdateTimestamp = false
|
|
}
|
|
const lastIndex = messages.findIndex(({ _id }) => _id === lastMsgBeforeFreeze)
|
|
if (lastIndex === -1) return
|
|
const firstNewMessage = messages.find(
|
|
({ createdBy }, index) => index > lastIndex && (createdBy === undefined || !socialStrings.includes(createdBy))
|
|
)
|
|
|
|
if (firstNewMessage === undefined) {
|
|
scrollToBottom()
|
|
return
|
|
}
|
|
|
|
const messagesElements = contentDiv?.getElementsByClassName('activityMessage')
|
|
const msgElement = messagesElements?.[firstNewMessage._id as any]
|
|
|
|
if (msgElement == null) return
|
|
|
|
const messageRect = msgElement.getBoundingClientRect()
|
|
const topOffset = messageRect.top - newSeparatorOffset
|
|
|
|
if (topOffset < 0) {
|
|
scroller?.scrollBy(topOffset)
|
|
} else if (scrollDiv.scrollTop > 0) {
|
|
scrollDiv.scroll({ top: 0, behavior: 'instant' })
|
|
}
|
|
}
|
|
|
|
function updateShouldScrollToNew (): void {
|
|
if (scrollDiv != null && contentDiv != null) {
|
|
const { scrollTop } = scrollDiv
|
|
const offset = 100
|
|
|
|
shouldScrollToNew = Math.abs(scrollTop) < offset
|
|
}
|
|
}
|
|
|
|
async function wait (): Promise<void> {
|
|
// One tick is not enough for messages to be rendered,
|
|
// I think this is due to the fact that we are using a Component, which takes some time to load,
|
|
// because after one tick I see spinners from Component
|
|
await tick() // wait until the DOM is updated
|
|
await tick() // wait until the DOM is updated
|
|
}
|
|
|
|
async function initializeScroll (
|
|
isLoading: boolean,
|
|
separatorElement?: HTMLDivElement | null,
|
|
separatorIndex?: number
|
|
): Promise<void> {
|
|
if (isLoading || isScrollInitialized) {
|
|
return
|
|
}
|
|
|
|
const selectedMessageExists =
|
|
selectedMessageId !== undefined && messages.some(({ _id }) => _id === selectedMessageId)
|
|
if (selectedMessageExists) {
|
|
await wait()
|
|
scrollToMessage()
|
|
isScrollInitialized = true
|
|
} else if (separatorIndex === -1) {
|
|
isScrollInitialized = true
|
|
shouldScrollToNew = true
|
|
isScrollAtBottom = true
|
|
} else if (separatorElement != null) {
|
|
await wait()
|
|
scrollToSeparator()
|
|
isScrollInitialized = true
|
|
}
|
|
|
|
if (isScrollInitialized) {
|
|
await wait()
|
|
updateSelectedDate()
|
|
updateScrollData()
|
|
updateDownButtonVisibility($metadataStore, messages, scrollDiv)
|
|
loadMore()
|
|
}
|
|
}
|
|
|
|
function reinitializeScroll (): void {
|
|
isScrollInitialized = false
|
|
void initializeScroll($isLoadingStore, separatorDiv, separatorIndex)
|
|
}
|
|
|
|
function handleJumpToDate (e: CustomEvent<{ date?: Timestamp }>): void {
|
|
const result = jumpToDate(e, provider, uuid, scrollDiv)
|
|
|
|
dateToJump = result.dateToJump
|
|
|
|
if (result.scrollOffset !== undefined && result.scrollOffset !== 0 && scroller != null) {
|
|
scroller?.scroll(result.scrollOffset)
|
|
}
|
|
}
|
|
|
|
function scrollToDate (date: Timestamp): void {
|
|
const offset = getScrollToDateOffset(date, uuid)
|
|
|
|
if (offset !== undefined && offset !== 0 && scroller != null) {
|
|
scroller?.scroll(offset)
|
|
dateToJump = undefined
|
|
}
|
|
}
|
|
|
|
function updateSelectedDate (): void {
|
|
if (isThread) return
|
|
selectedDate = getSelectedDate(provider, uuid, scrollDiv, contentDiv)
|
|
}
|
|
|
|
function read (): void {
|
|
if (isFreeze() || notifyContext === undefined || !isScrollInitialized) return
|
|
readViewportMessages(messages, notifyContext._id, scrollDiv, contentDiv)
|
|
}
|
|
|
|
function updateScrollData (): void {
|
|
if (scrollDiv == null) return
|
|
const { scrollTop } = scrollDiv
|
|
|
|
isScrollAtBottom = Math.abs(scrollTop) < 50
|
|
}
|
|
|
|
function canGroupChatMessages (message: ActivityMessage, prevMessage?: ActivityMessage): boolean {
|
|
let prevMetadata: MessageMetadata | undefined = undefined
|
|
|
|
if (prevMessage === undefined) {
|
|
const metadata = $metadataStore
|
|
prevMetadata = metadata.find((_, index) => metadata[index + 1]?._id === message._id)
|
|
}
|
|
|
|
return canGroupMessages(message, prevMessage ?? prevMetadata)
|
|
}
|
|
|
|
$: updateDownButtonVisibility($metadataStore, messages, scrollDiv)
|
|
|
|
function updateDownButtonVisibility (
|
|
metadata: MessageMetadata[],
|
|
messages: ActivityMessage[],
|
|
scrollDiv?: HTMLDivElement | null
|
|
): void {
|
|
if (metadata.length === 0 || messages.length === 0) {
|
|
isLatestMessageButtonVisible = false
|
|
return
|
|
}
|
|
|
|
if (!$isTailLoadedStore) {
|
|
isLatestMessageButtonVisible = true
|
|
} else if (scrollDiv != null) {
|
|
const { scrollTop } = scrollDiv
|
|
|
|
isLatestMessageButtonVisible = Math.abs(scrollTop) > 200
|
|
} else {
|
|
isLatestMessageButtonVisible = false
|
|
}
|
|
}
|
|
|
|
async function handleScrollToLatestMessage (): Promise<void> {
|
|
selectedMessageId = undefined
|
|
messageInFocus.set(undefined)
|
|
|
|
const metadata = $metadataStore
|
|
const lastMetadata = metadata[metadata.length - 1]
|
|
const lastMessage = messages[messages.length - 1]
|
|
|
|
if (lastMetadata._id !== lastMessage._id) {
|
|
separatorIndex = -1
|
|
provider.jumpToEnd(true)
|
|
reinitializeScroll()
|
|
} else {
|
|
scrollToBottom()
|
|
}
|
|
|
|
await inboxClient.readDoc(doc._id)
|
|
}
|
|
|
|
let forceRead = false
|
|
$: void forceReadContext(isScrollAtBottom, notifyContext)
|
|
|
|
async function forceReadContext (isScrollAtBottom: boolean, context?: DocNotifyContext): Promise<void> {
|
|
if (context === undefined || !isScrollAtBottom || forceRead || isFreeze()) return
|
|
const { lastUpdateTimestamp = 0, lastViewedTimestamp = 0 } = context
|
|
|
|
if (lastViewedTimestamp >= lastUpdateTimestamp) return
|
|
|
|
const notifications = $notificationsByContextStore.get(context._id) ?? []
|
|
const unViewed = notifications.filter(({ isViewed }) => !isViewed)
|
|
|
|
if (unViewed.length === 0) {
|
|
forceRead = true
|
|
await inboxClient.readDoc(object._id)
|
|
}
|
|
}
|
|
|
|
function shouldLoadMoreUp (): boolean {
|
|
if (scrollDiv == null) return false
|
|
const { scrollHeight, scrollTop, clientHeight } = scrollDiv
|
|
|
|
return scrollHeight + Math.ceil(scrollTop - clientHeight) <= loadMoreThreshold
|
|
}
|
|
|
|
function shouldLoadMoreDown (): boolean {
|
|
if (scrollDiv == null) return false
|
|
|
|
return Math.abs(scrollDiv.scrollTop) <= loadMoreThreshold
|
|
}
|
|
|
|
function loadMore (): void {
|
|
if (!loadMoreAllowed || $isLoadingMoreStore || scrollDiv == null || !isScrollInitialized) {
|
|
return
|
|
}
|
|
|
|
const minMsgHeightPx = minMsgHeightRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
|
const maxMsgPerScreen = Math.ceil(scrollDiv.clientHeight / minMsgHeightPx)
|
|
const limit = Math.max(maxMsgPerScreen, provider.limit)
|
|
const isLoadMoreUp = shouldLoadMoreUp()
|
|
const isLoadMoreDown = shouldLoadMoreDown()
|
|
|
|
if (!isLoadMoreUp && backwardRequested) {
|
|
backwardRequested = false
|
|
}
|
|
|
|
if (isLoadMoreUp && !backwardRequested) {
|
|
shouldScrollToNew = false
|
|
restoreScrollTop = scrollDiv?.scrollTop ?? 0
|
|
restoreScrollHeight = 0
|
|
void provider.addNextChunk('backward', messages[0]?.createdOn, limit)
|
|
backwardRequested = true
|
|
} else if (isLoadMoreUp && backwardRequested) {
|
|
restoreScrollTop = scrollDiv?.scrollTop ?? 0
|
|
} else if (isLoadMoreDown && !$isTailLoadedStore) {
|
|
restoreScrollTop = 0
|
|
restoreScrollHeight = scrollDiv?.scrollHeight ?? 0
|
|
shouldScrollToNew = false
|
|
isScrollAtBottom = false
|
|
void provider.addNextChunk('forward', messages[messages.length - 1]?.createdOn, limit)
|
|
}
|
|
}
|
|
|
|
async function restoreScroll (): Promise<void> {
|
|
await wait()
|
|
|
|
if (scrollDiv == null || scroller == null) return
|
|
|
|
if (restoreScrollTop !== 0) {
|
|
scroller.scroll(restoreScrollTop)
|
|
} else if (restoreScrollHeight !== 0) {
|
|
const delta = restoreScrollHeight - scrollDiv.scrollHeight
|
|
scroller.scroll(delta)
|
|
}
|
|
backwardRequested = false
|
|
restoreScrollHeight = 0
|
|
restoreScrollTop = 0
|
|
dateToJump = 0
|
|
}
|
|
|
|
function scrollToNewMessages (): void {
|
|
if (scrollDiv == null || !shouldScrollToNew) {
|
|
read()
|
|
return
|
|
}
|
|
|
|
scrollToBottom()
|
|
read()
|
|
}
|
|
|
|
async function handleMessagesUpdated (newCount: number): Promise<void> {
|
|
if (newCount === messagesCount) {
|
|
return
|
|
}
|
|
|
|
const prevCount = messagesCount
|
|
messagesCount = newCount
|
|
|
|
if (isFreeze()) {
|
|
await wait()
|
|
scrollToStartOfNew()
|
|
return
|
|
}
|
|
|
|
if (restoreScrollTop !== 0 || restoreScrollHeight !== 0) {
|
|
void restoreScroll()
|
|
} else if (dateToJump !== undefined) {
|
|
await wait()
|
|
scrollToDate(dateToJump)
|
|
} else if (shouldScrollToNew && prevCount > 0 && newCount > prevCount) {
|
|
await wait()
|
|
scrollToNewMessages()
|
|
} else {
|
|
await wait()
|
|
read()
|
|
}
|
|
}
|
|
|
|
async function handleScroll (): Promise<void> {
|
|
updateScrollData()
|
|
updateDownButtonVisibility($metadataStore, messages, scrollDiv)
|
|
updateShouldScrollToNew()
|
|
loadMore()
|
|
updateSelectedDate()
|
|
read()
|
|
}
|
|
|
|
function handleResize (): void {
|
|
if (!isScrollInitialized) return
|
|
if (shouldScrollToNew) {
|
|
scrollToBottom()
|
|
}
|
|
|
|
loadMore()
|
|
}
|
|
|
|
const newMessageTxListener = (txes: Tx[]): void => {
|
|
const ctx = txes
|
|
.map((it) => it as TxCUD<ActivityMessage>)
|
|
.filter((it) => it.attachedTo === doc._id && it._class === core.class.TxCreateDoc)
|
|
|
|
if (ctx.length > 0 && shouldScrollToNew) {
|
|
void wait().then(scrollToNewMessages)
|
|
}
|
|
}
|
|
|
|
afterUpdate(() => {
|
|
if (isFreeze()) {
|
|
updateScrollData()
|
|
}
|
|
})
|
|
|
|
onMount(() => {
|
|
chatReadMessagesStore.update(() => new Set())
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
window.addEventListener('focus', handleWindowFocus)
|
|
window.addEventListener('blur', handleWindowBlur)
|
|
addTxListener(newMessageTxListener)
|
|
})
|
|
|
|
onDestroy(() => {
|
|
unsubscribe()
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
window.removeEventListener('focus', handleWindowFocus)
|
|
window.removeEventListener('blur', handleWindowBlur)
|
|
removeTxListener(newMessageTxListener)
|
|
})
|
|
|
|
$: showBlankView = !$isLoadingStore && messages.length === 0 && !isThread
|
|
|
|
export function editLastMessage (): void {
|
|
if ($isLoadingStore || !isScrollInitialized || !$isTailLoadedStore || scrollDiv == null) return
|
|
if (!isScrollAtBottom) return
|
|
const me = getCurrentAccount()
|
|
let lastMessage: ChatMessage | undefined = undefined
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
const message = messages[i]
|
|
if (!hierarchy.isDerived(message._class, chunter.class.ChatMessage)) continue
|
|
if (message.createdBy == null || !me.socialIds.includes(message.createdBy)) continue
|
|
lastMessage = message as ChatMessage
|
|
break
|
|
}
|
|
|
|
if (lastMessage == null) return
|
|
editingMessageStore.set(lastMessage._id)
|
|
const messagesElements = contentDiv?.getElementsByClassName('activityMessage')
|
|
const msgElement = messagesElements?.[selectedMessageId as any]
|
|
if (msgElement == null) return
|
|
const scrollRect = scrollDiv.getBoundingClientRect()
|
|
|
|
if (!messageInView(msgElement, scrollRect)) {
|
|
msgElement.scrollIntoView({ behavior: 'instant', block: 'end' })
|
|
}
|
|
}
|
|
|
|
function handleKeyDown (e: KeyboardEvent): void {
|
|
const key = e.key
|
|
|
|
if (key === 'ArrowUp') {
|
|
if ($editingMessageStore !== undefined) return
|
|
editLastMessage()
|
|
}
|
|
}
|
|
function getKey (messages: ActivityMessage[]): string {
|
|
return `${messages.length}-${Math.max(...messages.map((m) => m.modifiedOn))}`
|
|
}
|
|
</script>
|
|
|
|
<div class="flex-col relative" class:h-full={fullHeight}>
|
|
{#if !isThread && messages.length > 0 && selectedDate}
|
|
<div class="selectedDate">
|
|
<JumpToDateSelector {selectedDate} fixed on:jumpToDate={handleJumpToDate} idPrefix={`${uuid}-`} />
|
|
</div>
|
|
{/if}
|
|
<BaseChatScroller
|
|
bind:scroller
|
|
bind:scrollDiv
|
|
bind:contentDiv
|
|
bottomStart={!showBlankView}
|
|
loadingOverlay={$isLoadingStore || !isScrollInitialized}
|
|
onScroll={handleScroll}
|
|
onResize={handleResize}
|
|
key={getKey(messages)}
|
|
>
|
|
{#if showBlankView}
|
|
<BlankView
|
|
icon={chunter.icon.Thread}
|
|
header={chunter.string.NoMessagesInChannel}
|
|
label={readonly ? undefined : chunter.string.SendMessagesInChannel}
|
|
/>
|
|
{/if}
|
|
|
|
{#if loadMoreAllowed && !isThread}
|
|
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
|
{/if}
|
|
|
|
<slot name="header" />
|
|
|
|
{#each messages as message, index (message._id)}
|
|
{@const isSelected = message._id === selectedMessageId}
|
|
{@const canGroup = canGroupChatMessages(message, messages[index - 1])}
|
|
{#if separatorIndex === index}
|
|
<ActivityMessagesSeparator bind:element={separatorDiv} label={activity.string.New} />
|
|
{/if}
|
|
|
|
{#if !isThread && message.createdOn && $datesStore.includes(message.createdOn)}
|
|
<JumpToDateSelector
|
|
idPrefix={`${uuid}-`}
|
|
visible={selectedDate !== message.createdOn}
|
|
selectedDate={message.createdOn}
|
|
on:jumpToDate={handleJumpToDate}
|
|
/>
|
|
{/if}
|
|
|
|
<ActivityMessagePresenter
|
|
{doc}
|
|
value={message}
|
|
skipLabel={isThread || isChunterSpace}
|
|
hideLink
|
|
hoverStyles="filledHover"
|
|
attachmentImageSize="x-large"
|
|
type={canGroup ? 'short' : 'default'}
|
|
isHighlighted={isSelected}
|
|
shouldScroll={false}
|
|
{readonly}
|
|
{onReply}
|
|
/>
|
|
{/each}
|
|
|
|
{#if messages.length > 0}
|
|
<div class="h-4" />
|
|
{/if}
|
|
|
|
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
|
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
|
{/if}
|
|
{#if !fixedInput && withInput && !readonly}
|
|
<ChannelInput
|
|
{object}
|
|
{readonly}
|
|
boundary={scrollDiv}
|
|
{collection}
|
|
{isThread}
|
|
{autofocus}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
{/if}
|
|
</BaseChatScroller>
|
|
{#if !isThread && isLatestMessageButtonVisible}
|
|
<div class="down-button absolute">
|
|
<ModernButton
|
|
label={chunter.string.LatestMessages}
|
|
shape="round"
|
|
size="small"
|
|
kind="primary"
|
|
on:click={handleScrollToLatestMessage}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if fixedInput && withInput && !readonly}
|
|
<ChannelInput
|
|
{object}
|
|
{readonly}
|
|
boundary={scrollDiv}
|
|
{collection}
|
|
{isThread}
|
|
{autofocus}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
{/if}
|
|
|
|
{#if readonly}
|
|
<div class="h-6" />
|
|
{/if}
|
|
|
|
<style lang="scss">
|
|
.selectedDate {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.down-button {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
bottom: 0.5rem;
|
|
animation: 0.5s fadeIn;
|
|
animation-fill-mode: forwards;
|
|
visibility: hidden;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
99% {
|
|
visibility: hidden;
|
|
}
|
|
100% {
|
|
visibility: visible;
|
|
}
|
|
}
|
|
</style>
|