Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-09-23 13:36:31 +07:00
commit ad3473acc2
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
23 changed files with 223 additions and 100 deletions

View File

@ -35,6 +35,7 @@
"@hcengineering/model": "^0.6.11",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11",

View File

@ -70,6 +70,7 @@ import preference, { TPreference } from '@hcengineering/model-preference'
import view from '@hcengineering/model-view'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import presentation from '@hcengineering/model-presentation'
import activity from './plugin'
import { buildActions } from './actions'
@ -375,6 +376,10 @@ export function createModel (builder: Builder): void {
]
})
builder.mixin(activity.class.Reaction, core.class.Class, presentation.mixin.InstantTransactions, {
txClasses: [core.class.TxCreateDoc]
})
buildActions(builder)
buildNotifications(builder)
}

View File

@ -98,7 +98,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
throw new Error('createDoc cannot be called for DOMAIN_MODEL classes with non-model space')
}
const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id, modifiedOn, modifiedBy)
await this.client.tx(tx)
await this.tx(tx)
return tx.objectId
}
@ -122,7 +122,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedOn,
modifiedBy
)
await this.client.tx(tx)
await this.tx(tx)
return tx.tx.objectId as unknown as Ref<P>
}
@ -147,7 +147,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedOn,
modifiedBy
)
await this.client.tx(tx)
await this.tx(tx)
return tx.objectId
}
@ -170,7 +170,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedOn,
modifiedBy
)
await this.client.tx(tx)
await this.tx(tx)
return tx.objectId
}
@ -184,7 +184,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve, modifiedOn, modifiedBy)
return this.client.tx(tx)
return this.tx(tx)
}
removeDoc<T extends Doc>(
@ -195,7 +195,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedBy?: Ref<Account>
): Promise<TxResult> {
const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId, modifiedOn, modifiedBy)
return this.client.tx(tx)
return this.tx(tx)
}
createMixin<D extends Doc, M extends D>(
@ -216,7 +216,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedOn,
modifiedBy
)
return this.client.tx(tx)
return this.tx(tx)
}
updateMixin<D extends Doc, M extends D>(
@ -237,7 +237,7 @@ export class TxOperations implements Omit<Client, 'notify'> {
modifiedOn,
modifiedBy
)
return this.client.tx(tx)
return this.tx(tx)
}
async update<T extends Doc>(

View File

@ -111,7 +111,7 @@ export function addNotification (
title,
subTitle,
severity,
position: NotificationPosition.TopRight,
position: NotificationPosition.BottomLeft,
component,
closeTimeout,
params

View File

@ -211,6 +211,11 @@
{
sort: {
createdOn: SortingOrder.Ascending
},
lookup: {
_id: {
reactions: activity.class.Reaction
}
}
}
)

View File

@ -39,7 +39,7 @@
$: void updateInlineActions(message, excludedActions)
savedMessagesStore.subscribe(() => {
void updateInlineActions(message)
void updateInlineActions(message, excludedActions)
})
function handleActionMenuOpened (): void {

View File

@ -69,6 +69,7 @@
attribute={attributeModel.attribute}
value={values[0]}
{prevValue}
withShowMore={false}
showOnlyDiff
/>
{/if}

View File

@ -55,7 +55,7 @@
ev.preventDefault()
ev.stopPropagation()
showPopup(EmojiPopup, {}, ev.target as HTMLElement, async (emoji: string) => {
await updateDocReactions(client, reactions, object, emoji)
await updateDocReactions(reactions, object, emoji)
})
}
</script>

View File

@ -14,40 +14,48 @@
-->
<script lang="ts">
import activity, { ActivityMessage, Reaction } from '@hcengineering/activity'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery } from '@hcengineering/presentation'
import { WithLookup } from '@hcengineering/core'
import { getSpace, updateDocReactions } from '../../utils'
import Reactions from './Reactions.svelte'
export let object: ActivityMessage | undefined
export let object: WithLookup<ActivityMessage> | undefined
export let readonly = false
const client = getClient()
const reactionsQuery = createQuery()
let reactions: Reaction[] = []
$: hasReactions = object?.reactions && object.reactions > 0
$: hasReactions = (object?.reactions ?? 0) > 0
$: lookupReactions = object?.$lookup?.reactions as Reaction[] | undefined
$: if (object && hasReactions) {
reactionsQuery.query(
activity.class.Reaction,
{ attachedTo: object._id, space: getSpace(object) },
(res: Reaction[]) => {
reactions = res
}
)
} else {
reactionsQuery.unsubscribe()
$: updateReactions(hasReactions, object, lookupReactions)
function updateReactions (hasReactions: boolean, object?: ActivityMessage, lookupReaction?: Reaction[]): void {
if (lookupReaction !== undefined) {
reactions = lookupReaction
} else if (object && hasReactions) {
reactionsQuery.query(
activity.class.Reaction,
{ attachedTo: object._id, space: getSpace(object) },
(res: Reaction[]) => {
reactions = res
}
)
} else {
reactionsQuery.unsubscribe()
reactions = []
}
}
const handleClick = (ev: CustomEvent) => {
if (readonly) return
void updateDocReactions(client, reactions, object, ev.detail)
void updateDocReactions(reactions, object, ev.detail)
}
</script>
{#if object && hasReactions}
{#if object && reactions.length > 0}
<div class="footer flex-col p-inline contrast mt-2 min-h-6">
<Reactions {reactions} {object} {readonly} on:click={handleClick} />
</div>

View File

@ -66,7 +66,7 @@
e.stopPropagation()
e.preventDefault()
showPopup(EmojiPopup, {}, e.target as HTMLElement, (emoji: string) => {
void updateDocReactions(client, reactions, message, emoji)
void updateDocReactions(reactions, message, emoji)
})
}
</script>

View File

@ -1,12 +1,5 @@
import type { ActivityMessage, Reaction } from '@hcengineering/activity'
import core, {
getCurrentAccount,
isOtherHour,
type Doc,
type Ref,
type TxOperations,
type Space
} from '@hcengineering/core'
import core, { getCurrentAccount, isOtherHour, type Doc, type Ref, type Space } from '@hcengineering/core'
import { getClient, isSpace } from '@hcengineering/presentation'
import {
EmojiPopup,
@ -22,18 +15,13 @@ import { get } from 'svelte/store'
import { savedMessagesStore } from './activity'
import activity from './plugin'
export async function updateDocReactions (
client: TxOperations,
reactions: Reaction[],
object?: Doc,
emoji?: string
): Promise<void> {
export async function updateDocReactions (reactions: Reaction[], object?: Doc, emoji?: string): Promise<void> {
if (emoji === undefined || object === undefined) {
return
}
const client = getClient()
const currentAccount = getCurrentAccount()
const reaction = reactions.find((r) => r.emoji === emoji && r.createBy === currentAccount._id)
if (reaction == null) {
@ -72,7 +60,7 @@ export async function addReactionAction (
closePopup()
showPopup(EmojiPopup, {}, element, (emoji: string) => {
void updateDocReactions(client, reactions, message, emoji)
void updateDocReactions(reactions, message, emoji)
params?.onClose?.()
})
params?.onOpen?.()

View File

@ -20,6 +20,7 @@ import {
type DocumentQuery,
getCurrentAccount,
isOtherDay,
type Lookup,
type Ref,
SortingOrder,
type Space,
@ -119,7 +120,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
})
constructor (
readonly context: DocNotifyContext | undefined,
private context: DocNotifyContext | undefined,
readonly space: Ref<Space>,
chatId: Ref<Doc>,
_class: Ref<Class<ActivityMessage>>,
@ -209,6 +210,13 @@ export class ChannelDataProvider implements IChannelDataProvider {
)
}
async updateNewTimestamp (context?: DocNotifyContext): Promise<void> {
this.context = context ?? this.context
const firstNewMsgIndex = await this.getFirstNewMsgIndex()
const metadata = get(this.metadataStore)
this.newTimestampStore.set(firstNewMsgIndex !== undefined ? metadata[firstNewMsgIndex]?.createdOn : undefined)
}
private async loadInitialMessages (
selectedMsg?: Ref<ActivityMessage>,
loadAll = false,
@ -285,13 +293,21 @@ export class ChannelDataProvider implements IChannelDataProvider {
},
{
sort: { createdOn: SortingOrder.Descending },
lookup: {
_id: { attachments: attachment.class.Attachment, inlineButtons: chunter.class.InlineButton }
}
lookup: this.getLookup()
}
)
}
getLookup (): Lookup<ActivityMessage> {
return {
_id: {
attachments: attachment.class.Attachment,
inlineButtons: chunter.class.InlineButton,
reactions: activity.class.Reaction
}
}
}
isNextLoading (mode: LoadMode): boolean {
return mode === 'forward' ? get(this.isForwardLoading) : get(this.isBackwardLoading)
}
@ -331,9 +347,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
{
limit: limit ?? this.limit,
sort: { createdOn: isBackward ? SortingOrder.Descending : SortingOrder.Ascending },
lookup: {
_id: { attachments: attachment.class.Attachment, inlineButtons: chunter.class.InlineButton }
}
lookup: this.getLookup()
}
)

View File

@ -26,7 +26,7 @@
messageInFocus,
sortActivityMessages
} from '@hcengineering/activity-resources'
import { Doc, getDay, Ref, Timestamp } from '@hcengineering/core'
import { Doc, getCurrentAccount, getDay, Ref, Timestamp } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
@ -72,6 +72,7 @@
const minMsgHeightRem = 2
const loadMoreThreshold = 40
const me = getCurrentAccount()
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = inboxClient.contextByDoc
@ -131,8 +132,23 @@
}
})
let isPageHidden = false
let lastMsgBeforeFreeze: Ref<ActivityMessage> | undefined = undefined
function handleVisibilityChange (): void {
if (document.hidden) {
isPageHidden = true
lastMsgBeforeFreeze = shouldScrollToNew ? displayMessages[displayMessages.length - 1]?._id : undefined
} else {
if (isPageHidden) {
isPageHidden = false
void provider.updateNewTimestamp(notifyContext)
}
}
}
function isFreeze (): boolean {
return freeze
return freeze || isPageHidden
}
$: displayMessages = filterChatMessages(messages, filters, filterResources, doc._class, selectedFilters)
@ -295,6 +311,38 @@
}
}
function scrollToStartOfNew (): void {
if (!scrollElement || !lastMsgBeforeFreeze) {
return
}
const lastIndex = displayMessages.findIndex(({ _id }) => _id === lastMsgBeforeFreeze)
if (lastIndex === -1) return
const firstNewMessage = displayMessages.find(({ createdBy }, index) => index > lastIndex && createdBy !== me._id)
if (firstNewMessage === undefined) {
scrollToBottom()
return
}
const messagesElements = scrollContentBox?.getElementsByClassName('activityMessage')
const msgElement = messagesElements?.[firstNewMessage._id as any]
if (!msgElement) {
return
}
const messageRect = msgElement.getBoundingClientRect()
const topOffset = messageRect.top - 150
if (topOffset < 0) {
scroller?.scrollBy(topOffset)
} else if (topOffset > 0) {
scroller?.scrollBy(topOffset)
}
}
async function handleScroll ({ autoScrolling }: ScrollParams): Promise<void> {
saveScrollPosition()
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
@ -339,7 +387,7 @@
return messageRect.top >= containerRect.top && messageRect.bottom - messageRect.height / 2 <= containerRect.bottom
}
const messagesToReadAccumulator: DisplayActivityMessage[] = []
const messagesToReadAccumulator: Set<DisplayActivityMessage> = new Set<DisplayActivityMessage>()
let messagesToReadAccumulatorTimer: any
function readViewportMessages (): void {
@ -359,13 +407,14 @@
}
if (messageInView(msgElement, containerRect)) {
messagesToReadAccumulator.push(message)
messagesToReadAccumulator.add(message)
}
}
clearTimeout(messagesToReadAccumulatorTimer)
messagesToReadAccumulatorTimer = setTimeout(() => {
const messagesToRead = [...messagesToReadAccumulator]
messagesToReadAccumulator.clear()
void readChannelMessages(sortActivityMessages(messagesToRead), notifyContext)
}, 500)
}
@ -555,8 +604,12 @@
return
}
const prevCount = messagesCount
messagesCount = newCount
if (isFreeze()) {
messagesCount = newCount
await wait()
scrollToStartOfNew()
return
}
@ -565,15 +618,13 @@
} else if (dateToJump !== undefined) {
await wait()
scrollToDate(dateToJump)
} else if (shouldScrollToNew && messagesCount > 0 && newCount > messagesCount) {
} else if (shouldScrollToNew && prevCount > 0 && newCount > prevCount) {
await wait()
scrollToNewMessages()
} else {
await wait()
readViewportMessages()
}
messagesCount = newCount
}
$: void handleMessagesUpdated(displayMessages.length)
@ -610,10 +661,12 @@
afterUpdate(() => {
if (!scrollElement) return
const { scrollHeight } = scrollElement
const { offsetHeight, scrollHeight, scrollTop } = scrollElement
if (!isInitialScrolling && prevScrollHeight < scrollHeight && isScrollAtBottom) {
if (!isInitialScrolling && !isFreeze() && prevScrollHeight < scrollHeight && isScrollAtBottom) {
scrollToBottom()
} else if (isFreeze()) {
isScrollAtBottom = scrollHeight <= Math.ceil(scrollTop + offsetHeight)
}
})
@ -641,10 +694,12 @@
onMount(() => {
chatReadMessagesStore.update(() => new Set())
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onDestroy(() => {
unsubscribe()
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
let showScrollDownButton = false
@ -715,7 +770,7 @@
const canLoadNextForwardStore = provider.canLoadNextForwardStore
$: if (!freeze) {
$: if (!freeze && !isPageHidden && isScrollInitialized) {
readViewportMessages()
}
</script>

View File

@ -37,14 +37,14 @@
$: savedMessages = $savedMessagesStore
$: savedAttachments = $savedAttachmentsStore
async function openAttachment (attach?: Attachment) {
async function openAttachment (attach?: Attachment): Promise<void> {
if (attach === undefined) {
return
}
const messageId: Ref<ActivityMessage> = attach.attachedTo as Ref<ActivityMessage>
await client.findOne(activity.class.ActivityMessage, { _id: messageId }).then((res) => {
if (res !== undefined) {
openMessageFromSpecial(res)
void openMessageFromSpecial(res)
}
})
}
@ -62,8 +62,8 @@
}
}
}
function handleMessageClicked (message?: ActivityMessage) {
openMessageFromSpecial(message)
function handleMessageClicked (message?: ActivityMessage): void {
void openMessageFromSpecial(message)
}
</script>

View File

@ -19,4 +19,11 @@
export let message: ActivityMessage
</script>
<ActivityMessagePresenter value={message} hideFooter hoverStyles="filledHover" withShowMore={false} skipLabel />
<ActivityMessagePresenter
value={message}
hideFooter
hoverStyles="filledHover"
withShowMore={false}
attachmentImageSize="x-large"
skipLabel
/>

View File

@ -19,6 +19,8 @@
import activity, { ActivityMessage } from '@hcengineering/activity'
import { PersonAccount } from '@hcengineering/contact'
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
import notification from '@hcengineering/notification'
import attachment from '@hcengineering/attachment'
import chunter from '../../plugin'
import Header from '../Header.svelte'
@ -32,15 +34,21 @@
$: threadsQuery.query(
activity.class.ActivityMessage,
{
replies: { $exists: true }
replies: { $exists: true },
[`${notification.mixin.Collaborators}.collaborators`]: me._id
},
(res) => {
threads = res.filter(
({ createdBy, repliedPersons, replies }) =>
(replies !== undefined && replies > 0 && createdBy === me._id) || repliedPersons?.includes(me.person)
)
threads = res.filter(({ replies }) => (replies ?? 0) > 0)
},
{ sort: { modifiedOn: SortingOrder.Descending } }
{
sort: { modifiedOn: SortingOrder.Descending },
lookup: {
_id: {
attachments: attachment.class.Attachment,
reactions: activity.class.Reaction
}
}
}
)
</script>

View File

@ -430,11 +430,7 @@ export async function readChannelMessages (
messages: DisplayActivityMessage[],
context: DocNotifyContext | undefined
): Promise<void> {
if (messages.length === 0) {
return
}
if (context === undefined) {
if (messages.length === 0 || context === undefined) {
return
}
@ -442,9 +438,7 @@ export async function readChannelMessages (
const client = getClient().apply(undefined, 'readViewportMessages')
try {
const readMessages = get(chatReadMessagesStore)
const allIds = getAllIds(messages).filter((id) => !readMessages.has(id))
const allIds = getAllIds(messages)
const notifications = get(inboxClient.activityInboxNotifications)
.filter(({ attachedTo, $lookup, isViewed }) => {
if (isViewed) return false

View File

@ -16,7 +16,12 @@
-->
<script lang="ts">
import activity, { ActivityMessage } from '@hcengineering/activity'
import { ActivityFilter, ActivityMessagePresenter, canGroupMessages } from '@hcengineering/activity-resources'
import {
ActivityFilter,
ActivityMessagePresenter,
canGroupMessages,
combineActivityMessages
} from '@hcengineering/activity-resources'
import { SortingOrder } from '@hcengineering/core'
import { Document } from '@hcengineering/document'
import { createQuery } from '@hcengineering/presentation'
@ -33,7 +38,9 @@
activity.class.ActivityMessage,
{ attachedTo: value._id, space: value.space },
(res) => {
messages = res
void combineActivityMessages(res).then((res) => {
messages = res
})
},
{
sort: {

View File

@ -23,6 +23,7 @@
export let value: Markup | undefined
export let prevValue: Markup | undefined = undefined
export let attribute: AnyAttribute | undefined = undefined
export let withShowMore = true
export let showOnlyDiff: boolean = false
@ -85,8 +86,14 @@
}
</script>
<ShowMore>
{#if withShowMore}
<ShowMore>
{#key [value, prevValue]}
<MarkupDiffViewer objectClass={attribute?.attributeOf} {content} {comparedVersion} />
{/key}
</ShowMore>
{:else}
{#key [value, prevValue]}
<MarkupDiffViewer objectClass={attribute?.attributeOf} {content} {comparedVersion} />
{/key}
</ShowMore>
{/if}

View File

@ -21,7 +21,8 @@
location as locationStore,
Location,
Header,
Breadcrumbs
Breadcrumbs,
getCurrentLocation
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
@ -45,13 +46,33 @@
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
$: tabId = widgetState?.tab
$: tabs = widgetState?.tabs ?? []
$: tabs = getTabs(widget, widgetState)
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
$: if ($sidebarStore.widget === undefined) {
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
}
function getTabs (widget?: Widget, state?: WidgetState): WidgetTab[] {
if (widget === undefined || !state?.tabs) return []
const loc = getCurrentLocation()
const result: WidgetTab[] = []
for (const tab of state.tabs) {
if (tab.allowedPath !== undefined && !tab.isPinned) {
const path = loc.path.join('/')
if (!path.startsWith(tab.allowedPath)) {
void handleTabClose(tab.id, widget)
continue
}
}
result.push(tab)
}
return result
}
const unsubscribe = locationStore.subscribe((loc: Location) => {
if (widget === undefined) return

View File

@ -248,7 +248,7 @@ export async function getPersonNotificationTxes (
...content,
docNotifyContext: context._id,
_id: generateId(),
_class: notification.class.CommonInboxNotification,
_class: notification.class.MentionInboxNotification,
space: receiverSpace._id,
modifiedOn: originTx.modifiedOn,
modifiedBy: sender._id

View File

@ -78,7 +78,7 @@ import serverNotification, {
SenderInfo
} from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
import { markupToHTML, markupToText, stripTags } from '@hcengineering/text'
import { markupToText, stripTags } from '@hcengineering/text'
import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
@ -240,8 +240,10 @@ export async function getContentByTemplate (
notificationData !== undefined &&
control.hierarchy.isDerived(notificationData._class, notification.class.MentionInboxNotification)
) {
const text = (notificationData as MentionInboxNotification).messageHtml
params.body = text !== undefined ? markupToHTML(text) : params.body
const messageContent = (notificationData as MentionInboxNotification).messageHtml
const text = messageContent !== undefined ? markupToText(messageContent) : undefined
params.body = text ?? params.body
params.message = text ?? params.message
}
if (message !== undefined) {

View File

@ -698,16 +698,16 @@ export class PlatformWorker {
errors++
return
}
if (workspaceInfo?.workspace === undefined) {
this.ctx.error('No workspace exists for workspaceId', { workspace })
errors++
return
}
if (workspaceInfo?.disabled === true) {
this.ctx.error('Workspace is disabled workspaceId', { workspace })
return
}
try {
if (workspaceInfo?.workspace === undefined) {
this.ctx.error('No workspace exists for workspaceId', { workspace })
errors++
return
}
if (workspaceInfo?.disabled === true) {
this.ctx.error('Workspace is disabled workspaceId', { workspace })
return
}
const branding = Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) ?? null
const workerCtx = this.ctx.newChild('worker', { workspace: workspaceInfo.workspace }, {})