UBERF-7638: Add scroll to latest message button (#6119)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-07-23 14:15:58 +04:00 committed by GitHub
parent c443cc60bd
commit 4962367182
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 135 additions and 38 deletions

View File

@ -40,6 +40,7 @@
export let inheritFont: boolean = false
export let tooltip: LabelAndProps | undefined = undefined
export let element: HTMLButtonElement | undefined = undefined
export let shape: 'rectangle' | 'round' = 'rectangle'
export let id: string | undefined = undefined
let actualIconSize: IconSize = 'small'
@ -89,7 +90,7 @@
<button
{id}
bind:this={element}
class="font-medium-14 {kind} {size} {type}"
class="font-medium-14 {kind} {size} {type} {shape}"
class:loading
class:pressed
class:inheritColor
@ -155,6 +156,10 @@
height: var(--global-large-Size);
border-radius: var(--medium-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
@ -167,6 +172,9 @@
height: var(--global-medium-Size);
border-radius: var(--medium-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-2);
}
@ -180,6 +188,9 @@
gap: var(--spacing-0_5);
border-radius: var(--small-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}
@ -192,6 +203,9 @@
height: var(--global-extra-small-Size);
border-radius: var(--extra-small-BorderRadius);
&.round {
border-radius: var(--large-BorderRadius);
}
&.type-button:not(.iconOnly) {
padding: 0 var(--spacing-1);
}

View File

@ -14,6 +14,7 @@
export let labelParams: Record<string, any> = {}
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
export let size: ButtonBaseSize = 'large'
export let shape: 'rectangle' | 'round' = 'rectangle'
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
export let iconSize: IconSize | undefined = undefined
export let disabled: boolean = false
@ -29,6 +30,7 @@
<ButtonBase
type={'type-button'}
{title}
{shape}
{label}
{labelParams}
{kind}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "Click \"Join\" to get started.",
"JoinChannelText": "Once you've joined, you'll be able to read all messages and contribute to the discussion.",
"NoMessagesInChannel": "Currently there are no messages",
"SendMessagesInChannel": "Send the first message to start the conversation"
"SendMessagesInChannel": "Send the first message to start the conversation",
"LatestMessages": "↓ Latest messages"
}
}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "Haga clic en \"Unirse\" para comenzar.",
"JoinChannelText": "Una vez que se haya unido, podrá leer todos los mensajes y contribuir a la discusión.",
"NoMessagesInChannel": "No hay mensajes en este canal todavía.",
"SendMessagesInChannel": "Envíe mensajes en este canal para comenzar la conversación."
"SendMessagesInChannel": "Envíe mensajes en este canal para comenzar la conversación.",
"LatestMessages": "↓ Últimos mensajes"
}
}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "Cliquez sur \"Rejoindre\" pour commencer.",
"JoinChannelText": "Une fois que vous avez rejoint, vous pourrez lire tous les messages et participer à la discussion.",
"NoMessagesInChannel": "Il n'y a pas encore de messages dans ce canal.",
"SendMessagesInChannel": "Envoyez des messages pour commencer la conversation."
"SendMessagesInChannel": "Envoyez des messages pour commencer la conversation.",
"LatestMessages": "↓ Derniers messages"
}
}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "Clique em \"Participar\" para começar.",
"JoinChannelText": "Depois de entrar, você poderá ler todas as mensagens e contribuir na discussão.",
"NoMessagesInChannel": "Ainda não existem mensagens neste canal.",
"SendMessagesInChannel": "Envie a sua primeira mensagem!"
"SendMessagesInChannel": "Envie a sua primeira mensagem!",
"LatestMessages": "↓ Últimas mensagens"
}
}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "Нажмите \"Присоединиться\", чтобы начать.",
"JoinChannelText": "Присоединившись, вы сможете читать все сообщения и участвовать в обсуждении.",
"NoMessagesInChannel": "В этом канале пока нет сообщений",
"SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение"
"SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение",
"LatestMessages": "↓ Последние сообщения"
}
}

View File

@ -110,6 +110,7 @@
"JoinChannelHeader": "点击“加入”开始。",
"JoinChannelText": "加入后,你将能够阅读所有消息并参与讨论。",
"NoMessagesInChannel": "此频道中没有消息。",
"SendMessagesInChannel": "在此频道中发送消息。"
"SendMessagesInChannel": "在此频道中发送消息。",
"LatestMessages": "↓ 最新消息"
}
}

View File

@ -29,10 +29,10 @@ import { derived, get, type Readable, writable } from 'svelte/store'
import { type ActivityMessage } from '@hcengineering/activity'
import attachment from '@hcengineering/attachment'
import { combineActivityMessages } from '@hcengineering/activity-resources'
import { type ChatMessage } from '@hcengineering/chunter'
import notification, { type DocNotifyContext } from '@hcengineering/notification'
import chunter from './plugin'
import { type ChatMessage } from '@hcengineering/chunter'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
export type LoadMode = 'forward' | 'backward'
@ -175,7 +175,11 @@ export class ChannelDataProvider implements IChannelDataProvider {
)
}
private async loadInitialMessages (selectedMsg?: Ref<ActivityMessage>, loadAll = false): Promise<void> {
private async loadInitialMessages (
selectedMsg?: Ref<ActivityMessage>,
loadAll = false,
ignoreNew = false
): Promise<void> {
const isLoading = get(this.isInitialLoadingStore)
const isLoaded = get(this.isInitialLoadedStore)
@ -183,28 +187,15 @@ export class ChannelDataProvider implements IChannelDataProvider {
return
}
const client = getClient()
this.isInitialLoadingStore.set(true)
const firstNotification =
this.context !== undefined
? await client.findOne(
notification.class.InboxNotification,
{
_class: {
$in: [notification.class.MentionInboxNotification, notification.class.ActivityInboxNotification]
},
docNotifyContext: this.context._id,
isViewed: false
},
{ sort: { createdOn: SortingOrder.Ascending } }
)
: undefined
const metadata = get(this.metadataStore)
const firstNewMsgIndex = this.getFirstNewMsgIndex(firstNotification)
const firstNewMsgIndex = ignoreNew ? undefined : await this.getFirstNewMsgIndex()
if (get(this.newTimestampStore) === undefined) {
this.newTimestampStore.set(firstNewMsgIndex !== undefined ? metadata[firstNewMsgIndex]?.createdOn : undefined)
} else if (ignoreNew) {
this.newTimestampStore.set(undefined)
}
const startPosition = this.getStartPosition(selectedMsg ?? this.selectedMsgId, firstNewMsgIndex)
@ -351,7 +342,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
return firsNewMsgIndex
}
private getFirstNewMsgIndex (firstNotification: InboxNotification | undefined): number | undefined {
private async getFirstNewMsgIndex (): Promise<number | undefined> {
const metadata = get(this.metadataStore)
if (metadata.length === 0) {
@ -363,6 +354,18 @@ export class ChannelDataProvider implements IChannelDataProvider {
}
const lastViewedTimestamp = this.context.lastViewedTimestamp
const client = getClient()
const firstNotification = await client.findOne(
notification.class.InboxNotification,
{
_class: {
$in: [notification.class.MentionInboxNotification, notification.class.ActivityInboxNotification]
},
docNotifyContext: this.context._id,
isViewed: false
},
{ sort: { createdOn: SortingOrder.Ascending } }
)
if (lastViewedTimestamp === undefined && firstNotification === undefined) {
return -1
@ -436,7 +439,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
await this.loadInitialMessages(msg._id)
}
public jumpToMessage (message: ActivityMessage): boolean {
public jumpToMessage (message: MessageMetadata): boolean {
const metadata = get(this.metadataStore).find(({ _id }) => _id === message._id)
if (metadata === undefined) {
@ -455,7 +458,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
return true
}
public jumpToEnd (): boolean {
public jumpToEnd (ignoreNew = false): boolean {
const last = get(this.metadataStore)[get(this.metadataStore).length - 1]
if (last === undefined) {
@ -468,8 +471,9 @@ export class ChannelDataProvider implements IChannelDataProvider {
return false
}
this.selectedMsgId = undefined
this.clearMessages()
void this.loadInitialMessages()
void this.loadInitialMessages(this.selectedMsgId, false, ignoreNew)
return true
}

View File

@ -22,13 +22,14 @@
import {
ActivityExtension as ActivityExtensionComponent,
ActivityMessagePresenter,
canGroupMessages
canGroupMessages,
messageInFocus
} from '@hcengineering/activity-resources'
import { Class, Doc, getDay, Ref, Timestamp } from '@hcengineering/core'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Loading, Scroller, ScrollParams } from '@hcengineering/ui'
import { Loading, ModernButton, Scroller, ScrollParams } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, onDestroy, onMount, tick } from 'svelte'
import { get } from 'svelte/store'
@ -101,6 +102,9 @@
let selectedDate: Timestamp | undefined = undefined
let dateToJump: Timestamp | undefined = undefined
let prevScrollHeight = 0
let isScrollAtBottom = false
let messagesCount = 0
let wasAsideOpened = isAsideOpened
@ -277,6 +281,7 @@
function handleScroll ({ autoScrolling }: ScrollParams): void {
saveScrollPosition()
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
if (autoScrolling) {
return
}
@ -450,6 +455,8 @@
isScrollInitialized = true
isInitialScrolling = false
}
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
}
function reinitializeScroll (): void {
@ -557,9 +564,6 @@
loadMore()
}
let prevScrollHeight = 0
let isScrollAtBottom = false
function saveScrollPosition (): void {
if (!scrollElement) {
return
@ -588,7 +592,7 @@
}
})
async function compensateAside (isOpened: boolean) {
async function compensateAside (isOpened: boolean): Promise<void> {
if (!isInitialScrolling && isScrollAtBottom && !wasAsideOpened && isOpened) {
await wait()
scrollToBottom()
@ -599,7 +603,7 @@
$: void compensateAside(isAsideOpened)
function canGroupChatMessages (message: ActivityMessage, prevMessage?: ActivityMessage) {
function canGroupChatMessages (message: ActivityMessage, prevMessage?: ActivityMessage): boolean {
let prevMetadata: MessageMetadata | undefined = undefined
if (prevMessage === undefined) {
@ -617,6 +621,53 @@
onDestroy(() => {
unsubscribe()
})
let showScrollDownButton = false
$: updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
function updateDownButtonVisibility (
metadata: MessageMetadata[],
displayMessages: DisplayActivityMessage[],
element?: HTMLDivElement
): void {
if (metadata.length === 0 || displayMessages.length === 0) {
showScrollDownButton = false
return
}
const lastMetadata = metadata[metadata.length - 1]
const lastMessage = displayMessages[displayMessages.length - 1]
if (lastMetadata._id !== lastMessage._id) {
showScrollDownButton = true
} else if (element != null) {
const { scrollHeight, scrollTop, offsetHeight } = element
showScrollDownButton = scrollHeight > offsetHeight + scrollTop + 300
} else {
showScrollDownButton = false
}
}
function handleScrollDown (): void {
selectedMessageId = undefined
messageInFocus.set(undefined)
const metadata = $metadataStore
const lastMetadata = metadata[metadata.length - 1]
const lastMessage = displayMessages[displayMessages.length - 1]
void inboxClient.readDoc(client, objectId)
if (lastMetadata._id !== lastMessage._id) {
separatorIndex = -1
provider.jumpToEnd(true)
reinitializeScroll()
} else {
scrollToBottom()
}
}
</script>
{#if isLoading}
@ -689,6 +740,18 @@
<HistoryLoading isLoading={$isLoadingMoreStore} />
{/if}
</Scroller>
{#if showScrollDownButton}
<div class="down-button absolute">
<ModernButton
label={chunter.string.LatestMessages}
shape="round"
size="small"
kind="primary"
on:click={handleScrollDown}
/>
</div>
{/if}
</div>
{#if object}
<div class="ref-input">
@ -728,4 +791,11 @@
left: 0;
right: 0;
}
.down-button {
width: 100%;
display: flex;
justify-content: center;
bottom: -0.75rem;
}
</style>

View File

@ -108,6 +108,7 @@ export default mergeIds(chunterId, chunter, {
ArchiveActivityConfirmationTitle: '' as IntlString,
ArchiveActivityConfirmationMessage: '' as IntlString,
JoinChannelHeader: '' as IntlString,
JoinChannelText: '' as IntlString
JoinChannelText: '' as IntlString,
LatestMessages: '' as IntlString
}
})