UBERF-6829: group messages of the same type and user (#5569)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-05-10 15:58:02 +04:00 committed by GitHub
parent 07b200a558
commit c0348bf313
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 146 additions and 45 deletions

View File

@ -20,6 +20,12 @@ export function getDay (time: Timestamp): Timestamp {
return date.getTime()
}
export function getHour (time: Timestamp): Timestamp {
const date: Date = new Date(time)
date.setMinutes(0, 0, 0)
return date.getTime()
}
export function getDisplayTime (time: number): string {
let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
if (!isToday(time)) {
@ -37,6 +43,10 @@ export function isOtherDay (time1: Timestamp, time2: Timestamp): boolean {
return getDay(time1) !== getDay(time2)
}
export function isOtherHour (time1: Timestamp, time2: Timestamp): boolean {
return getHour(time1) !== getHour(time2)
}
function isToday (time: number): boolean {
const current = new Date()
const target = new Date(time)

View File

@ -21,6 +21,7 @@
import ActivityExtensionComponent from './ActivityExtension.svelte'
import ActivityFilter from './ActivityFilter.svelte'
import { combineActivityMessages } from '../activityMessagesUtils'
import { canGroupMessages } from '../utils'
export let object: Doc
export let showCommenInput: boolean = true
@ -95,14 +96,16 @@
<div class="p-activity select-text" id={activity.string.Activity} class:newest-first={isNewestFirst}>
{#if filteredMessages.length}
<Grid column={1} rowGap={0}>
{#each filteredMessages as message}
{#each filteredMessages as message, index}
{@const canGroup = canGroupMessages(message, filteredMessages[index - 1])}
<Lazy>
<Component
is={activity.component.ActivityMessagePresenter}
props={{
value: message,
hideLink: true,
boundary
boundary,
type: canGroup ? 'short' : 'default'
}}
/>
</Lazy>

View File

@ -19,6 +19,7 @@
import { getEmbeddedLabel } from '@hcengineering/platform'
export let date: Timestamp
export let shortTime = false
$: fullDate = new Date(date).toLocaleString('default', {
minute: '2-digit',
@ -27,8 +28,14 @@
month: 'short',
year: 'numeric'
})
function getShortTime (date: Timestamp): string {
const options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
return new Date(date).toLocaleTimeString('default', options).split(' ')[0]
}
</script>
<span class="text-sm" use:tooltip={{ label: getEmbeddedLabel(fullDate) }}>
{getDisplayTime(date)}
{shortTime ? getShortTime(date) : getDisplayTime(date)}
</span>

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DisplayActivityMessage } from '@hcengineering/activity'
import { DisplayActivityMessage, ActivityMessageViewType } from '@hcengineering/activity'
import view from '@hcengineering/view'
import { getClient } from '@hcengineering/presentation'
import { Action, Component } from '@hcengineering/ui'
@ -34,6 +34,7 @@
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let withShowMore: boolean = true
export let attachmentImageSize: 'x-large' | undefined = undefined
export let type: ActivityMessageViewType = 'default'
export let showLinksPreview = true
export let videoPreload = true
export let hideLink = false
@ -68,6 +69,7 @@
showLinksPreview,
videoPreload,
hideLink,
type,
compact,
onClick
}}

View File

@ -13,7 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityMessageViewlet, DisplayActivityMessage } from '@hcengineering/activity'
import activity, {
ActivityMessageViewlet,
DisplayActivityMessage,
ActivityMessageViewType
} from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
import core from '@hcengineering/core'
@ -47,6 +51,7 @@
export let hoverable = true
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let showDatePreposition = false
export let type: ActivityMessageViewType = 'default'
export let onClick: (() => void) | undefined = undefined
const client = getClient()
@ -93,6 +98,12 @@
let readonly: boolean = false
$: readonly = $restrictionStore.disableComments
function canDisplayShort (type: ActivityMessageViewType, isSaved: boolean): boolean {
return type === 'short' && !isSaved && (message.replies ?? 0) === 0
}
$: isShort = canDisplayShort(type, isSaved)
</script>
{#if !isHidden}
@ -119,10 +130,16 @@
isActionsOpened = true
}}
>
{#if showNotify && !embedded}
{#if showNotify && !embedded && !isShort}
<div class="notify" />
{/if}
{#if !embedded}
{#if embedded}
<div class="embeddedMarker" />
{:else if isShort}
<span class="text-sm lower time">
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} shortTime />
</span>
{:else}
<div class="min-w-6 mt-1 relative">
{#if $$slots.icon}
<slot name="icon" />
@ -137,33 +154,33 @@
</div>
{/if}
</div>
{:else}
<div class="embeddedMarker" />
{/if}
<div class="flex-col ml-2 w-full clear-mins message-content">
<div class="header clear-mins">
{#if person}
<EmployeePresenter value={person} shouldShowAvatar={false} compact />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
{#if !isShort}
<div class="header clear-mins">
{#if person}
<EmployeePresenter value={person} shouldShowAvatar={false} compact />
{:else}
<div class="strong">
<Label label={core.string.System} />
</div>
{/if}
{#if !skipLabel}
<slot name="header" />
{/if}
{#if !skipLabel}
<slot name="header" />
{/if}
{#if !skipLabel && showDatePreposition}
<span class="text-sm lower">
<Label label={activity.string.At} />
</span>
{/if}
{#if !skipLabel && showDatePreposition}
<span class="text-sm lower">
<Label label={activity.string.At} />
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
</span>
{/if}
<span class="text-sm lower">
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
</span>
</div>
</div>
{/if}
<slot name="content" />
@ -203,7 +220,7 @@
position: relative;
display: flex;
flex-shrink: 0;
padding: 0.75rem 0.75rem 0.75rem 1rem;
padding: 0.5rem 0.75rem 0.5rem 1rem;
gap: 1rem;
//overflow: hidden;
border: 1px solid transparent;
@ -243,6 +260,18 @@
visibility: visible;
}
&:hover > .time {
visibility: visible;
}
.time {
display: flex;
justify-content: end;
width: 2.5rem;
visibility: hidden;
margin-top: 0.125rem;
}
&.actionsOpened {
&.borderedHover {
border: 1px solid var(--global-ui-BackgroundColor);

View File

@ -15,6 +15,7 @@
<script lang="ts">
import activity, {
ActivityMessage,
ActivityMessageViewType,
DisplayActivityMessage,
DisplayDocUpdateMessage,
DocUpdateMessage,
@ -35,6 +36,7 @@
import DocUpdateMessageHeader from './DocUpdateMessageHeader.svelte'
import { getAttributeModel, getCollectionAttribute } from '../../activityMessagesUtils'
import { getIsTextType } from '../../utils'
export let value: DisplayDocUpdateMessage
export let showNotify: boolean = false
@ -50,6 +52,7 @@
export let hoverable = true
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let hideLink = false
export let type: ActivityMessageViewType = 'default'
export let onClick: (() => void) | undefined = undefined
const client = getClient()
@ -169,6 +172,7 @@
{skipLabel}
{hoverable}
{hoverStyles}
type={viewlet?.label || getIsTextType(attributeModel) ? 'default' : type}
showDatePreposition={hideLink}
{onClick}
>

View File

@ -17,7 +17,8 @@ import core, {
TxProcessor,
type TxUpdateDoc,
matchQuery,
getCurrentAccount
getCurrentAccount,
isOtherHour
} from '@hcengineering/core'
import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
@ -520,9 +521,36 @@ export async function unpinMessage (message?: ActivityMessage): Promise<void> {
await client.update(message, { isPinned: false })
}
export function getIsTextType (attributeModel: AttributeModel): boolean {
export function getIsTextType (attributeModel?: AttributeModel): boolean {
if (attributeModel === undefined) {
return false
}
return (
attributeModel.attribute?.type?._class === core.class.TypeMarkup ||
attributeModel.attribute?.type?._class === core.class.TypeCollaborativeMarkup
)
}
const groupMessagesThresholdMs = 15 * 60 * 1000
type MessageData = Pick<ActivityMessage, '_class' | 'createdBy' | 'createdOn' | 'modifiedOn'>
export function canGroupMessages (message: MessageData, prevMessage?: MessageData): boolean {
if (prevMessage === undefined) {
return false
}
if (message.createdBy !== prevMessage.createdBy || message._class !== prevMessage._class) {
return false
}
const time1 = message.createdOn ?? message.modifiedOn
const time2 = prevMessage.createdOn ?? prevMessage.modifiedOn
if (isOtherHour(time1, time2)) {
return false
}
return time1 - time2 < groupMessagesThresholdMs
}

View File

@ -313,6 +313,7 @@ export interface UserMentionInfo extends AttachedDoc {
export interface IgnoreActivity extends Class<Doc> {}
export type ActivityMessagePreviewType = 'full' | 'content-only'
export type ActivityMessageViewType = 'default' | 'short'
export default plugin(activityId, {
mixin: {

View File

@ -34,9 +34,11 @@ import chunter from './plugin'
export type LoadMode = 'forward' | 'backward'
interface MessageMetadata {
export interface MessageMetadata {
_id: Ref<ActivityMessage>
_class: Ref<Class<ActivityMessage>>
createdOn?: Timestamp
modifiedOn: Timestamp
createdBy?: Ref<Account>
}
@ -54,6 +56,7 @@ interface IChannelDataProvider {
messagesStore: Readable<ActivityMessage[]>
newTimestampStore: Readable<Timestamp | undefined>
datesStore: Readable<Timestamp[]>
metadataStore: Readable<MessageMetadata[]>
loadMore: (mode: LoadMode, loadAfter: Timestamp) => Promise<void>
canLoadMore: (mode: LoadMode, loadAfter: Timestamp) => boolean
@ -72,7 +75,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
private selectedMsgId: Ref<ActivityMessage> | undefined = undefined
private tailStart: Timestamp | undefined = undefined
private readonly metadataStore = writable<MessageMetadata[]>([])
public readonly metadataStore = writable<MessageMetadata[]>([])
private readonly tailStore = writable<ActivityMessage[]>([])
private readonly chunksStore = writable<Chunk[]>([])
@ -166,7 +169,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
void this.loadInitialMessages(undefined, loadAll)
},
{
projection: { _id: 1, createdOn: 1, createdBy: 1, attachedTo: 1 },
projection: { _id: 1, _class: 1, createdOn: 1, createdBy: 1, attachedTo: 1, modifiedOn: 1 },
sort: { createdOn: SortingOrder.Ascending }
}
)

View File

@ -24,7 +24,8 @@
import { Loading, Scroller, ScrollParams } from '@hcengineering/ui'
import {
ActivityExtension as ActivityExtensionComponent,
ActivityMessagePresenter
ActivityMessagePresenter,
canGroupMessages
} from '@hcengineering/activity-resources'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { get } from 'svelte/store'
@ -34,7 +35,7 @@
import ActivityMessagesSeparator from './ChannelMessagesSeparator.svelte'
import { filterChatMessages, getClosestDate, readChannelMessages } from '../utils'
import HistoryLoading from './LoadingHistory.svelte'
import { ChannelDataProvider } from '../channelDataProvider'
import { ChannelDataProvider, MessageMetadata } from '../channelDataProvider'
import JumpToDateSelector from './JumpToDateSelector.svelte'
export let provider: ChannelDataProvider
@ -71,6 +72,7 @@
const isLoadingMoreStore = provider.isLoadingMoreStore
const newTimestampStore = provider.newTimestampStore
const datesStore = provider.datesStore
const metadataStore = provider.metadataStore
let messages: ActivityMessage[] = []
let displayMessages: DisplayActivityMessage[] = []
@ -553,6 +555,17 @@
}
$: void compensateAside(isAsideOpened)
function canGroupChatMessages (message: ActivityMessage, prevMessage?: ActivityMessage) {
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)
}
</script>
{#if isLoading}
@ -589,6 +602,7 @@
{#each displayMessages as message, index (message._id)}
{@const isSelected = message._id === selectedMessageId}
{@const canGroup = canGroupChatMessages(message, displayMessages[index - 1])}
{#if separatorIndex === index}
<ActivityMessagesSeparator bind:element={separatorElement} label={activity.string.New} />
@ -609,6 +623,7 @@
withShowMore={false}
attachmentImageSize="x-large"
showLinksPreview={false}
type={canGroup ? 'short' : 'default'}
hideLink
/>
</div>
@ -633,7 +648,6 @@
<style lang="scss">
.msg {
margin: 0;
min-height: 4.375rem;
height: auto;
display: flex;
flex-direction: column;

View File

@ -21,6 +21,7 @@
import chunter, { ChatMessage } from '@hcengineering/chunter'
import { closeTooltip, Label, Lazy, Spinner, resizeObserver, MiniToggle } from '@hcengineering/ui'
import { ObjectPresenter, DocNavLink } from '@hcengineering/view-resources'
import { canGroupMessages } from '@hcengineering/activity-resources'
import ChatMessageInput from './ChatMessageInput.svelte'
import ChatMessagePresenter from './ChatMessagePresenter.svelte'
@ -83,10 +84,11 @@
<Spinner />
</div>
{:else}
{#each messages as message}
{#each messages as message, index}
{@const canGroup = canGroupMessages(message, messages[index - 1])}
<div class="item">
<Lazy>
<ChatMessagePresenter value={message} hideLink />
<ChatMessagePresenter value={message} hideLink type={canGroup ? 'short' : 'default'} />
</Lazy>
</div>
{/each}
@ -131,10 +133,6 @@
.item {
max-width: 30rem;
}
.item + .item {
margin-top: 0.75rem;
}
}
.input {

View File

@ -21,7 +21,7 @@
import { getDocLinkTitle, LinkPresenter } from '@hcengineering/view-resources'
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
import view from '@hcengineering/view'
import activity, { DisplayActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessageViewType, DisplayActivityMessage } from '@hcengineering/activity'
import { ActivityDocLink, ActivityMessageTemplate } from '@hcengineering/activity-resources'
import chunter, { ChatMessage, ChatMessageViewlet } from '@hcengineering/chunter'
import { Attachment } from '@hcengineering/attachment'
@ -49,6 +49,7 @@
export let videoPreload = true
export let hideLink = false
export let compact = false
export let type: ActivityMessageViewType = 'default'
export let onClick: (() => void) | undefined = undefined
const client = getClient()
@ -181,6 +182,7 @@
{hoverStyles}
{skipLabel}
showDatePreposition={hideLink}
{type}
{onClick}
>
<svelte:fragment slot="header">

View File

@ -121,7 +121,7 @@
loadMoreAllowed={false}
>
<svelte:fragment slot="header">
<div class="mt-3 mr-6 ml-6">
<div class="mt-3">
<ThreadParentMessage {message} />
</div>
<div class="separator">