mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
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:
parent
07b200a558
commit
c0348bf313
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}}
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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 }
|
||||
}
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user