Fixed scroll to new messages on chat open (#5369)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2024-04-16 17:26:39 +07:00
committed by GitHub
parent d81f91fc3b
commit 84de2c1931
11 changed files with 98 additions and 51 deletions
+9 -2
View File
@@ -34,7 +34,8 @@ import {
type IgnoreActivity,
type Reaction,
type SavedMessage,
type TxViewlet
type TxViewlet,
type ReplyProvider
} from '@hcengineering/activity'
import contact, { type Person } from '@hcengineering/contact'
import core, {
@@ -264,6 +265,11 @@ export class TActivityMessagePreview extends TClass implements ActivityMessagePr
presenter!: AnyComponent
}
@Model(activity.class.ReplyProvider, core.class.Doc, DOMAIN_MODEL)
export class TReplyProvider extends TDoc implements ReplyProvider {
function!: Resource<(message: ActivityMessage) => Promise<void>>
}
export function createModel (builder: Builder): void {
builder.createModel(
TTxViewlet,
@@ -281,7 +287,8 @@ export function createModel (builder: Builder): void {
TSavedMessage,
TIgnoreActivity,
TActivityReference,
TActivityMessagePreview
TActivityMessagePreview,
TReplyProvider
)
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
+4
View File
@@ -733,6 +733,10 @@ export function createModel (builder: Builder, options = { addApplication: true
domain: DOMAIN_CHUNTER,
disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }]
})
builder.createDoc(activity.class.ReplyProvider, core.space.Model, {
function: chunter.function.ReplyToThread
})
}
export default chunter
+2 -1
View File
@@ -99,7 +99,8 @@ export default mergeIds(chunterId, chunter, {
CanDeleteMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
ReplyToThread: '' as Resource<(doc: ActivityMessage) => Promise<void>>
},
filter: {
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
@@ -13,10 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { personByIdStore, Avatar } from '@hcengineering/contact-resources'
import { Doc, IdMap, Ref, WithLookup } from '@hcengineering/core'
import { Label, TimeSince } from '@hcengineering/ui'
import activity, { ActivityMessage } from '@hcengineering/activity'
import notification, {
ActivityInboxNotification,
DocNotifyContext,
@@ -24,14 +25,13 @@
InboxNotificationsClient
} from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { Label, TimeSince, getLocation, navigate } from '@hcengineering/ui'
import { buildThreadLink } from '../navigation'
import { getClient } from '@hcengineering/presentation'
export let object: ActivityMessage
export let embedded = false
export let onReply: (() => void) | undefined = undefined
const client = getClient()
const maxDisplayPersons = 5
$: lastReply = object.lastReply ?? new Date().getTime()
@@ -77,7 +77,9 @@
.slice(0, maxDisplayPersons - 1)
}
function handleReply (e: MouseEvent): void {
const replyProvider = client.getModel().findAllSync(activity.class.ReplyProvider, {})[0]
async function handleReply (e: MouseEvent) {
e.stopPropagation()
e.preventDefault()
@@ -86,7 +88,10 @@
return
}
navigate(buildThreadLink(getLocation(), object.attachedTo, object.attachedToClass, object._id))
if (replyProvider) {
const fn = await getResource(replyProvider.function)
fn(object)
}
}
</script>
@@ -26,13 +26,13 @@
import { getActions, restrictionStore } from '@hcengineering/view-resources'
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
import ActivityMessageExtensionComponent from './ActivityMessageExtension.svelte'
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
import ActivityMessageActions from '../ActivityMessageActions.svelte'
import { isReactionMessage } from '../../activityMessagesUtils'
import Bookmark from '../icons/Bookmark.svelte'
import { savedMessagesStore } from '../../activity'
import MessageTimestamp from '../MessageTimestamp.svelte'
import Replies from '../Replies.svelte'
export let message: DisplayActivityMessage
export let parentMessage: DisplayActivityMessage | undefined = undefined
@@ -85,11 +85,7 @@
setTimeout(scrollToMessage, 100)
}
void client
.findAll(activity.class.ActivityMessageExtension, { ofMessage: message._class })
.then((res: ActivityMessageExtension[]) => {
extensions = res
})
$: extensions = client.getModel().findAllSync(activity.class.ActivityMessageExtension, { ofMessage: message._class })
function handleActionsOpened (): void {
isActionsOpened = true
@@ -175,11 +171,7 @@
<slot name="content" />
{#if !hideFooter}
<ActivityMessageExtensionComponent
kind="footer"
{extensions}
props={{ object: message, embedded, onReply }}
/>
<Replies {embedded} object={message} {onReply} />
{/if}
<ReactionsPresenter object={message} {readonly} />
{#if parentMessage && showEmbedded}
+6 -1
View File
@@ -308,6 +308,10 @@ export interface SavedMessage extends Preference {
attachedTo: Ref<ActivityMessage>
}
export interface ReplyProvider extends Doc {
function: Resource<(message: ActivityMessage) => Promise<void>>
}
/**
* @public
*/
@@ -334,7 +338,8 @@ export default plugin(activityId, {
ActivityExtension: '' as Ref<Class<ActivityExtension>>,
Reaction: '' as Ref<Class<Reaction>>,
SavedMessage: '' as Ref<Class<SavedMessage>>,
ActivityReference: '' as Ref<Class<ActivityReference>>
ActivityReference: '' as Ref<Class<ActivityReference>>,
ReplyProvider: '' as Ref<Class<ReplyProvider>>
},
icon: {
Activity: '' as Asset,
@@ -29,6 +29,7 @@
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { get } from 'svelte/store'
import { tick, beforeUpdate, afterUpdate } from 'svelte'
import { getResource } from '@hcengineering/platform'
import ActivityMessagesSeparator from './ChannelMessagesSeparator.svelte'
import { filterChatMessages, getClosestDate, readChannelMessages } from '../utils'
@@ -58,7 +59,12 @@
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = inboxClient.contextByDoc
const filters = client.getModel().findAllSync(activity.class.ActivityMessagesFilter, {})
let filters: ActivityMessagesFilter[] = []
const filterResources = new Map<
Ref<ActivityMessagesFilter>,
(message: ActivityMessage, _class?: Ref<Doc>) => boolean
>()
const messagesStore = provider.messagesStore
const isLoadingStore = provider.isLoadingStore
@@ -93,15 +99,23 @@
$: notifyContext = $contextByDocStore.get(objectId)
$: void filterChatMessages(messages, filters, objectClass, selectedFilters).then((filteredMessages) => {
displayMessages = filteredMessages
})
void client
.getModel()
.findAll(activity.class.ActivityMessagesFilter, {})
.then(async (res) => {
filters = res
for (const filter of filters) {
filterResources.set(filter._id, await getResource(filter.filter))
}
})
$: displayMessages = filterChatMessages(messages, filters, filterResources, objectClass, selectedFilters)
inboxClient.inboxNotificationsByContext.subscribe(() => {
readViewportMessages()
})
function scrollToBottom (afterScrollFn?: () => void) {
function scrollToBottom (afterScrollFn?: () => void): void {
if (scroller !== undefined && scrollElement !== undefined) {
scroller.scrollBy(scrollElement.scrollHeight)
updateSelectedDate()
@@ -109,9 +123,19 @@
}
}
function scrollToSeparator () {
if (separatorElement) {
function scrollToSeparator (): void {
if (separatorElement && scrollElement) {
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const messagesHeight = displayMessages
.slice(separatorIndex)
.reduce((res, msg) => res + (messagesElements?.[msg._id as any]?.clientHeight ?? 0), 0)
separatorElement.scrollIntoView()
if (messagesHeight >= scrollElement.clientHeight) {
scroller?.scrollBy(-50)
}
updateShouldScrollToNew()
readViewportMessages()
}
@@ -373,7 +397,7 @@
newTimestamp !== undefined
? displayMessages.findIndex((message) => (message.createdOn ?? 0) >= (newTimestamp ?? 0))
: -1
$: void initializeScroll($isLoadingStore, separatorElement, separatorIndex)
$: void initializeScroll(isLoading, separatorElement, separatorIndex)
let isInitialScrolling = true
async function initializeScroll (isLoading: boolean, separatorElement?: HTMLDivElement, separatorIndex?: number) {
@@ -389,6 +413,7 @@
scrollToMessage()
isInitialScrolling = false
} else if (separatorIndex === -1) {
await wait()
isScrollInitialized = true
shouldWaitAndRead = true
autoscroll = true
@@ -24,13 +24,14 @@
Space
} from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import presentation, { createQuery } from '@hcengineering/presentation'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, Button, Icon, Label, Scroller, SearchEdit, showPopup } from '@hcengineering/ui'
import { FilterBar, FilterButton, SpacePresenter } from '@hcengineering/view-resources'
import workbench from '@hcengineering/workbench'
import { Channel } from '@hcengineering/chunter'
import { openChannel } from '../../../navigation'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { openChannel } from '../../../navigation'
import { getObjectIcon, joinChannel, leaveChannel } from '../../../utils'
import chunter from './../../../plugin'
@@ -45,6 +46,9 @@
const me = getCurrentAccount()._id
const channelsQuery = createQuery()
const client = getClient()
const notificationsClient = InboxNotificationsClientImpl.getClient()
const sort: SortingQuery<Space> = {
name: SortingOrder.Ascending
}
+3 -4
View File
@@ -48,7 +48,6 @@ import ChannelIcon from './components/ChannelIcon.svelte'
import ThreadNotificationPresenter from './components/notification/ThreadNotificationPresenter.svelte'
import ChatMessageNotificationLabel from './components/notification/ChatMessageNotificationLabel.svelte'
import ChatAside from './components/chat/ChatAside.svelte'
import Replies from './components/Replies.svelte'
import ReplyToThreadAction from './components/ReplyToThreadAction.svelte'
import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte'
import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte'
@@ -65,7 +64,7 @@ import {
leaveChannelAction,
removeChannelAction
} from './utils'
import { chunterSpaceLinkFragmentProvider, getThreadLink, getMessageLink } from './navigation'
import { chunterSpaceLinkFragmentProvider, getThreadLink, getMessageLink, replyToThread } from './navigation'
export { default as ChatMessagesPresenter } from './components/chat-message/ChatMessagesPresenter.svelte'
export { default as ChatMessagePopup } from './components/chat-message/ChatMessagePopup.svelte'
@@ -175,7 +174,6 @@ export default async (): Promise<Resources> => ({
ChatMessageNotificationLabel,
ThreadNotificationPresenter,
ChatAside,
Replies,
ReplyToThreadAction,
ThreadMessagePreview,
ChatMessagePreview
@@ -192,7 +190,8 @@ export default async (): Promise<Resources> => ({
CanCopyMessageLink: canCopyMessageLink,
GetChunterSpaceLinkFragment: chunterSpaceLinkFragmentProvider,
GetUnreadThreadsCount: getUnreadThreadsCount,
GetThreadLink: getThreadLink
GetThreadLink: getThreadLink,
ReplyToThread: replyToThread
},
actionImpl: {
ArchiveChannel,
+8 -5
View File
@@ -29,7 +29,7 @@ import {
} from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent } from '@hcengineering/ui'
import { type Asset, getResource, translate } from '@hcengineering/platform'
import { type Asset, translate } from '@hcengineering/platform'
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import activity, {
type ActivityMessage,
@@ -273,12 +273,13 @@ export function getClosestDate (selectedDate: Timestamp, dates: Timestamp[]): Ti
return closestDate
}
export async function filterChatMessages (
export function filterChatMessages (
messages: DisplayActivityMessage[],
filters: ActivityMessagesFilter[],
filterResources: Map<Ref<ActivityMessagesFilter>, (message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
objectClass: Ref<Class<Doc>>,
selectedIds: Array<Ref<ActivityMessagesFilter>>
): Promise<DisplayActivityMessage[]> {
): DisplayActivityMessage[] {
if (selectedIds.length === 0 || selectedIds.includes(activity.ids.AllFilter)) {
return messages
}
@@ -291,8 +292,10 @@ export async function filterChatMessages (
const filtersFns: Array<(message: ActivityMessage, _class?: Ref<Doc>) => boolean> = []
for (const filter of selectedFilters) {
const filterFn = await getResource(filter.filter)
filtersFns.push(filterFn)
const filterFn = filterResources.get(filter._id)
if (filterFn !== undefined) {
filtersFns.push(filterFn)
}
}
return messages.filter((message) => filtersFns.some((filterFn) => filterFn(message, objectClass)))
+12 -10
View File
@@ -432,6 +432,16 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
)
}
if (added.length > 0) {
res.push(
control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
$push: {
collaborators: { $each: added, $position: 0 }
}
})
)
}
for (const addedMember of added) {
const context = allContexts.find(({ user }) => user === addedMember)
@@ -457,16 +467,8 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
const contextsToRemove = allContexts.filter(({ user }) => removed.includes(user))
if (contextsToRemove.length === 0) {
return res
}
const channel = (await control.findAll(chunter.class.Channel, { _id: tx.objectId }))[0]
if (channel !== undefined) {
for (const context of contextsToRemove) {
res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id))
}
for (const context of contextsToRemove) {
res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id))
}
return res