[UBER-686] Chat in inbox (v2) (#3583)

Signed-off-by: Oleg Solodkov <oleg.solodkov@xored.com>
This commit is contained in:
Oleg Solodkov 2023-08-22 11:52:10 +07:00 committed by GitHub
parent f18bc8e700
commit 4c38e920d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 798 additions and 262 deletions

View File

@ -17787,7 +17787,7 @@ packages:
dev: false
file:projects/chunter.tgz:
resolution: {integrity: sha512-P+ZhImPhzQJDSPEIR+zEXNT87Rn3CvGG+7JN1OsF+3tVdNBnlkCQ/qrmK70Xp0tktXzolaB54lITf9luPQn/Tw==, tarball: file:projects/chunter.tgz}
resolution: {integrity: sha512-YvadGb0k4jR7JApOhvPTLZKTm3fX68dE9zX4j0GvCI6XrkUzZdD4oTa1IF8uuA3DRpHSt5pjNF8p1mI1ptK+BA==, tarball: file:projects/chunter.tgz}
name: '@rush-temp/chunter'
version: 0.0.0
dependencies:
@ -17800,6 +17800,7 @@ packages:
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
fast-equals: 2.0.4
prettier: 2.8.8
typescript: 4.8.4
transitivePeerDependencies:
@ -19106,7 +19107,7 @@ packages:
dev: false
file:projects/model-notification.tgz_typescript@4.8.4:
resolution: {integrity: sha512-vZSXHfSkfeklcsrLofIc/OuwLLUj6gT4o9Jl91MgLWeXjiIzaqZLBN6EcGegQ7nu9DU+8tWv0iZcWR6bpqWqcQ==, tarball: file:projects/model-notification.tgz}
resolution: {integrity: sha512-4TdxvQk1W0Q+g6eF8APHXTVp+ZZFHo2/HjnZZ7nYYqbEJy4EJUGvcCRXhbjsPqAgmoC80Y+D/GVdttR4fJKdnA==, tarball: file:projects/model-notification.tgz}
id: file:projects/model-notification.tgz
name: '@rush-temp/model-notification'
version: 0.0.0
@ -19928,7 +19929,7 @@ packages:
dev: false
file:projects/notification-resources.tgz_a1d864769aaf53d09b76fe134ab55e60:
resolution: {integrity: sha512-q3n0DYJDx1EejE47b3uDDVn9cKjcqNCkm502u0+TR6C3owM0rWljB9s0PGAZmJ66VWtglB0LMszUhJ1IqX4n0w==, tarball: file:projects/notification-resources.tgz}
resolution: {integrity: sha512-m2wYJypLkf9adSBx5NAW/1RJ7soDNp/VJb6WJk8vfdTfsIK9ttrm321UGN5FoWg3DP6H/UvCZObBN344cKno/w==, tarball: file:projects/notification-resources.tgz}
id: file:projects/notification-resources.tgz
name: '@rush-temp/notification-resources'
version: 0.0.0
@ -19941,6 +19942,7 @@ packages:
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
eslint-plugin-svelte3: 4.0.0_eslint@8.27.0+svelte@3.55.1
fast-equals: 2.0.4
prettier: 2.8.8
prettier-plugin-svelte: 2.8.0_prettier@2.8.8+svelte@3.55.1
sass: 1.56.1

View File

@ -25,7 +25,8 @@ import {
Message,
Reaction,
SavedMessages,
ThreadMessage
ThreadMessage,
DirectMessageInput
} from '@hcengineering/chunter'
import contact, { Person } from '@hcengineering/contact'
import type { Account, Class, Doc, Domain, Ref, Space, Timestamp } from '@hcengineering/core'
@ -35,6 +36,7 @@ import {
Builder,
Collection,
Index,
Mixin,
Model,
Prop,
ReadOnly,
@ -45,12 +47,13 @@ import {
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core'
import core, { TAttachedDoc, TClass, TSpace } from '@hcengineering/model-core'
import notification from '@hcengineering/model-notification'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import chunter from './plugin'
import { AnyComponent } from '@hcengineering/ui'
export { chunterId } from '@hcengineering/chunter'
export { chunterOperation } from './migration'
@ -158,6 +161,11 @@ export class TSavedMessages extends TPreference implements SavedMessages {
attachedTo!: Ref<ChunterMessage>
}
@Mixin(chunter.mixin.DirectMessageInput, core.class.Class)
export class TDirectMessageInput extends TClass implements DirectMessageInput {
component!: AnyComponent
}
export function createModel (builder: Builder, options = { addApplication: true }): void {
builder.createModel(
TChunterSpace,
@ -169,7 +177,8 @@ export function createModel (builder: Builder, options = { addApplication: true
TBacklink,
TDirectMessage,
TSavedMessages,
TReaction
TReaction,
TDirectMessageInput
)
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
@ -213,6 +222,14 @@ export function createModel (builder: Builder, options = { addApplication: true
presenter: chunter.component.DmPresenter
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.NotificationPreview, {
presenter: chunter.component.ChannelPreview
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, chunter.mixin.DirectMessageInput, {
component: chunter.component.DirectMessageInput
})
builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.NotificationObjectPresenter, {
presenter: chunter.component.ThreadParentPresenter
})

View File

@ -31,7 +31,6 @@ export default mergeIds(chunterId, chunter, {
MessagePresenter: '' as AnyComponent,
DmPresenter: '' as AnyComponent,
Threads: '' as AnyComponent,
ThreadView: '' as AnyComponent,
SavedMessages: '' as AnyComponent,
ChunterBrowser: '' as AnyComponent
},

View File

@ -37,6 +37,7 @@
"@hcengineering/workbench": "^0.6.8",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/setting": "^0.6.9"
"@hcengineering/setting": "^0.6.9",
"@hcengineering/chunter": "^0.6.10"
}
}

View File

@ -43,6 +43,7 @@ import {
NotificationGroup,
notificationId,
NotificationObjectPresenter,
NotificationPreview,
NotificationProvider,
NotificationSetting,
NotificationStatus,
@ -54,7 +55,7 @@ import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import notification from './plugin'
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
export { notificationId } from '@hcengineering/notification'
export { notificationOperation } from './migration'
export { notification as default }
@ -148,6 +149,11 @@ export class TNotificationObjectPresenter extends TClass implements Notification
presenter!: AnyComponent
}
@Mixin(notification.mixin.NotificationPreview, core.class.Class)
export class TNotificationPreview extends TClass implements NotificationPreview {
presenter!: AnyComponent
}
@Model(notification.class.DocUpdates, core.class.Doc, DOMAIN_NOTIFICATION)
export class TDocUpdates extends TDoc implements DocUpdates {
@Index(IndexKind.Indexed)
@ -175,7 +181,8 @@ export function createModel (builder: Builder): void {
TClassCollaborators,
TCollaborators,
TDocUpdates,
TNotificationObjectPresenter
TNotificationObjectPresenter,
TNotificationPreview
)
// Temporarily disabled, we should think about it
@ -230,7 +237,8 @@ export function createModel (builder: Builder): void {
icon: notification.icon.Notifications,
alias: notificationId,
hidden: true,
component: notification.component.Inbox
component: notification.component.Inbox,
aside: chunter.component.ThreadView
},
notification.app.Notification
)
@ -344,6 +352,21 @@ export function createModel (builder: Builder): void {
},
notification.ids.TxCollaboratorsChange
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: chunter.class.DirectMessage,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: notification.activity.TxDmCreation,
display: 'inline',
editable: false,
hideOnRemove: true
},
notification.ids.TxDmCreation
)
}
export function generateClassNotificationTypes (

View File

@ -35,10 +35,12 @@ export default mergeIds(notificationId, notification, {
Notification: '' as Ref<Application>
},
activity: {
TxCollaboratorsChange: '' as AnyComponent
TxCollaboratorsChange: '' as AnyComponent,
TxDmCreation: '' as AnyComponent
},
ids: {
TxCollaboratorsChange: '' as Ref<TxViewlet>
TxCollaboratorsChange: '' as Ref<TxViewlet>,
TxDmCreation: '' as Ref<TxViewlet>
},
component: {
NotificationSettings: '' as AnyComponent

View File

@ -45,6 +45,14 @@ export function createModel (builder: Builder): void {
trigger: serverChunter.trigger.ChunterTrigger
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.OnDmCreate,
txMatch: {
objectClass: chunter.class.DirectMessage,
_class: core.class.TxCreateDoc
}
})
builder.mixin(chunter.ids.DMNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, {
func: serverChunter.function.IsDirectMessage
})

View File

@ -71,6 +71,8 @@
"DMNotification": "Sent you a message",
"ConfigLabel": "Chat",
"ConfigDescription": "Extension to perform text communications",
"LastMessage": "Last message"
"LastMessage": "Last message",
"You": "You",
"YouHaveStartedAConversation": "You have started a conversation"
}
}

View File

@ -71,6 +71,8 @@
"DMNotification": "Отправил сообщение",
"ConfigLabel": "Чат",
"ConfigDescription": "Расширение для текстовых переписок",
"LastMessage": "Последнее сообщение"
"LastMessage": "Последнее сообщение",
"You": "Вы",
"YouHaveStartedAConversation": "Вы начали диалог"
}
}

View File

@ -1,4 +1,4 @@
import { Backlink } from '@hcengineering/chunter'
import { Backlink, getBacklinks } from '@hcengineering/chunter'
import contact, { PersonAccount } from '@hcengineering/contact'
import { Account, Class, Client, Data, Doc, DocumentQuery, Ref, TxOperations } from '@hcengineering/core'
import chunter from './plugin'
@ -33,81 +33,6 @@ export function isToday (time: number): boolean {
)
}
function extractBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
message: string,
kids: NodeListOf<ChildNode>
): Array<Data<Backlink>> {
const result: Array<Data<Backlink>> = []
const nodes: Array<NodeListOf<ChildNode>> = [kids]
while (true) {
const nds = nodes.shift()
if (nds === undefined) {
break
}
nds.forEach((kid) => {
if (
kid.nodeType === Node.ELEMENT_NODE &&
(kid as HTMLElement).localName === 'span' &&
(kid as HTMLElement).getAttribute('data-type') === 'reference'
) {
const el = kid as HTMLElement
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message: el.parentElement?.innerHTML ?? '',
attachedDocId
})
}
}
nodes.push(kid.childNodes)
})
}
return result
}
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Array<Data<Backlink>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
}
export async function createBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}
/**
* @public
*/

View File

@ -0,0 +1,69 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import chunter, { ChunterMessage, DirectMessage } from '@hcengineering/chunter'
import attachment from '@hcengineering/attachment'
import { Label } from '@hcengineering/ui'
import chunterResources from '../plugin'
import MessagePreview from './MessagePreview.svelte'
export let object: DirectMessage
export let newTxes: number
const NUM_OF_RECENT_MESSAGES = 5 as const
let messages: ChunterMessage[] = []
const messagesQuery = createQuery()
$: messagesQuery.query(
chunter.class.ChunterMessage,
{ attachedTo: object._id },
(res) => {
if (res !== undefined) {
messages = res.sort((a, b) => (a.createdOn ?? 0) - (b.createdOn ?? 0))
}
},
{
limit: newTxes + NUM_OF_RECENT_MESSAGES,
sort: {
createdOn: SortingOrder.Descending
},
lookup: {
_id: { attachments: attachment.class.Attachment }
}
}
)
</script>
<div class="flex-col flex-gap-3 preview-container">
{#if messages.length}
{#each messages as message}
<MessagePreview value={message} />
{/each}
{:else}
<Label label={chunterResources.string.YouHaveStartedAConversation} />
{/if}
</div>
<style lang="scss">
.preview-container {
padding: 0.5rem;
background-color: var(--theme-bg-color);
border: 1px solid var(--theme-divider-color);
border-radius: 0.25rem;
}
</style>

View File

@ -15,11 +15,10 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { ChunterMessage, ChunterSpace, Message } from '@hcengineering/chunter'
import { ChunterMessage, ChunterSpace, Message, createBacklinks } from '@hcengineering/chunter'
import { Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getLocation, navigate } from '@hcengineering/ui'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import Channel from './Channel.svelte'
import PinnedMessages from './PinnedMessages.svelte'

View File

@ -15,10 +15,9 @@
-->
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Comment } from '@hcengineering/chunter'
import { Comment, createBacklinks } from '@hcengineering/chunter'
import { AttachedData, Doc, generateId, Ref } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
export let object: Doc

View File

@ -0,0 +1,65 @@
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { DirectMessage, Message, createBacklinks, getDirectChannel } from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import { Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
export let account: PersonAccount
export let loading: boolean = true
const client = getClient()
const me = getCurrentAccount()._id
const _class = chunter.class.Message
let messageId = generateId() as Ref<Message>
let space: Ref<DirectMessage> | undefined
$: _getDirectChannel(account?._id)
async function _getDirectChannel (account?: Ref<PersonAccount>): Promise<void> {
if (account === undefined) {
return
}
space = await getDirectChannel(client, me as Ref<PersonAccount>, account)
}
async function onMessage (event: CustomEvent) {
if (space === undefined) {
return
}
const { message, attachments } = event.detail
await client.addCollection(
_class,
space,
space,
chunter.class.DirectMessage,
'messages',
{
content: message,
createBy: me,
attachments
},
messageId
)
await createBacklinks(client, space, chunter.class.ChunterSpace, messageId, message)
messageId = generateId()
}
</script>
{#if space !== undefined}
<div class="reference">
<AttachmentRefInput bind:loading {space} {_class} objectId={messageId} on:message={onMessage} />
</div>
{/if}
<style lang="scss">
.reference {
margin: 1.25rem 2.5rem;
}
</style>

View File

@ -28,7 +28,7 @@
import { createEventDispatcher } from 'svelte'
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin'
import { getTime } from '../utils'
import { getLinks, getTime } from '../utils'
// import Share from './icons/Share.svelte'
import notification, { Collaborators } from '@hcengineering/notification'
import Bookmark from './icons/Bookmark.svelte'
@ -200,24 +200,6 @@
$: links = getLinks(message.content)
function getLinks (content: string): HTMLLinkElement[] {
const parser = new DOMParser()
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
return parseLinks(parent.childNodes)
}
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
const res: HTMLLinkElement[] = []
nodes.forEach((p) => {
if (p.nodeType !== Node.TEXT_NODE) {
if (p.nodeName === 'A') {
res.push(p as HTMLLinkElement)
}
res.push(...parseLinks(p.childNodes))
}
})
return res
}
let loading = false
</script>

View File

@ -0,0 +1,119 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { ChunterMessage } from '@hcengineering/chunter'
import { MessageViewer } from '@hcengineering/presentation'
import ui, { Label, tooltip } from '@hcengineering/ui'
import { LinkPresenter } from '@hcengineering/view-resources'
import { AttachmentList } from '@hcengineering/attachment-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Ref, WithLookup, getCurrentAccount } from '@hcengineering/core'
import { Attachment } from '@hcengineering/attachment'
import { EmployeePresenter, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { PersonAccount } from '@hcengineering/contact'
import chunter from '../plugin'
import { getLinks, getTime } from '../utils'
export let value: WithLookup<ChunterMessage>
$: attachments = (value.$lookup?.attachments ?? []) as Attachment[]
$: links = getLinks(value.content)
const me = getCurrentAccount()._id as Ref<PersonAccount>
let account: PersonAccount | undefined
$: account = $personAccountByIdStore.get(value.createdBy as Ref<PersonAccount>)
$: employee = account && $personByIdStore.get(account.person)
</script>
<div class="container clear-mins" class:highlighted={false} id={value._id}>
<div class="message clear-mins">
<div class="flex-row-center header clear-mins">
{#if employee && account}
{#if account._id !== me}
<EmployeePresenter value={employee} shouldShowAvatar={true} disabled />
{:else}
<Label label={chunter.string.You} />
{/if}
{/if}
<span>{getTime(value.createdOn ?? 0)}</span>
{#if value.editedOn}
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getTime(value.editedOn) } }}>
<Label label={getEmbeddedLabel('Edited')} />
</span>
{/if}
</div>
<div class="text"><MessageViewer message={value.content} /></div>
{#if value.attachments}
<div class="attachments">
<AttachmentList {attachments} />
</div>
{/if}
{#each links as link}
<LinkPresenter {link} />
{/each}
</div>
</div>
<style lang="scss">
@keyframes highlight {
50% {
background-color: var(--theme-warning-color);
}
}
.container {
position: relative;
display: flex;
flex-shrink: 0;
padding: 0.5rem 0.15rem;
&.highlighted {
animation: highlight 2000ms ease-in-out;
}
.message {
display: flex;
flex-direction: column;
width: 100%;
margin-left: 1rem;
.header {
display: flex;
font-weight: 500;
line-height: 150%;
color: var(--theme-caption-color);
margin-bottom: 0.25rem;
span {
margin-left: 0.5rem;
font-weight: 400;
line-height: 1.125rem;
opacity: 0.4;
}
}
.text {
line-height: 150%;
user-select: contain;
}
.attachments {
margin-top: 0.25rem;
}
}
}
</style>

View File

@ -15,14 +15,14 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterSpace, Message, ThreadMessage } from '@hcengineering/chunter'
import { createBacklinks, type ChunterSpace, type Message, type ThreadMessage } from '@hcengineering/chunter'
import contact, { Person, PersonAccount, getName } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import core, { FindOptions, IdMap, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import ChannelPresenter from './ChannelPresenter.svelte'
import DmPresenter from './DmPresenter.svelte'

View File

@ -15,14 +15,14 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterMessage, Message, ThreadMessage } from '@hcengineering/chunter'
import { createBacklinks, type ChunterMessage, type Message, type ThreadMessage } from '@hcengineering/chunter'
import core, { Doc, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { IconClose, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, createEventDispatcher } from 'svelte'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import { isMessageHighlighted, messageIdForScroll, scrollAndHighLight, shouldScrollToMessage } from '../utils'
import ChannelSeparator from './ChannelSeparator.svelte'

View File

@ -44,8 +44,11 @@ import CreateDirectMessage from './components/CreateDirectMessage.svelte'
import DirectMessagePresenter from './components/DirectMessagePresenter.svelte'
import DmHeader from './components/DmHeader.svelte'
import DmPresenter from './components/DmPresenter.svelte'
import DirectMessageInput from './components/DirectMessageInput.svelte'
import EditChannel from './components/EditChannel.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import ChannelPreview from './components/ChannelPreview.svelte'
import MessagePreview from './components/MessagePreview.svelte'
import SavedMessages from './components/SavedMessages.svelte'
import ThreadParentPresenter from './components/ThreadParentPresenter.svelte'
import ThreadView from './components/ThreadView.svelte'
@ -64,7 +67,7 @@ import { getDmName, getLink, getTitle, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
export { CommentPopup, CommentsPresenter }
export { createBacklinks, updateBacklinks } from './backlinks'
export { updateBacklinks } from './backlinks'
async function MarkUnread (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()
@ -281,9 +284,12 @@ export default async (): Promise<Resources> => ({
ChannelPresenter,
DirectMessagePresenter,
MessagePresenter,
MessagePreview,
ChannelPreview,
ChunterBrowser,
DmHeader,
DmPresenter,
DirectMessageInput,
EditChannel,
Threads,
ThreadView,

View File

@ -28,7 +28,10 @@ export default mergeIds(chunterId, chunter, {
ChannelViewPanel: '' as AnyComponent,
ThreadViewPanel: '' as AnyComponent,
ThreadParentPresenter: '' as AnyComponent,
EditChannel: '' as AnyComponent
EditChannel: '' as AnyComponent,
ChannelPreview: '' as AnyComponent,
MessagePreview: '' as AnyComponent,
DirectMessageInput: '' as AnyComponent
},
function: {
GetDmName: '' as Resource<(client: Client, space: Space) => Promise<string>>
@ -84,6 +87,8 @@ export default mergeIds(chunterId, chunter, {
ChunterBrowser: '' as IntlString,
Messages: '' as IntlString,
NoResults: '' as IntlString,
CopyLink: '' as IntlString
CopyLink: '' as IntlString,
You: '' as IntlString,
YouHaveStartedAConversation: '' as IntlString
}
})

View File

@ -261,3 +261,22 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Reso
function isShortId (shortLink: string): boolean {
return /^\S+-\S+$/.test(shortLink)
}
export function getLinks (content: string): HTMLLinkElement[] {
const parser = new DOMParser()
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
return parseLinks(parent.childNodes)
}
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
const res: HTMLLinkElement[] = []
nodes.forEach((p) => {
if (p.nodeType !== Node.TEXT_NODE) {
if (p.nodeName === 'A') {
res.push(p as HTMLLinkElement)
}
res.push(...parseLinks(p.childNodes))
}
})
return res
}

View File

@ -26,6 +26,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"fast-equals": "^2.0.3",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/notification": "^0.6.14",

View File

@ -14,7 +14,17 @@
//
import type { Person } from '@hcengineering/contact'
import type { Account, AttachedDoc, Class, Doc, Ref, RelatedDocument, Space, Timestamp } from '@hcengineering/core'
import type {
Account,
AttachedDoc,
Class,
Doc,
Mixin,
Ref,
RelatedDocument,
Space,
Timestamp
} from '@hcengineering/core'
import { NotificationType } from '@hcengineering/notification'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
@ -113,11 +123,20 @@ export interface SavedMessages extends Preference {
attachedTo: Ref<ChunterMessage>
}
/**
* @public
*/
export interface DirectMessageInput extends Class<Doc> {
component: AnyComponent
}
/**
* @public
*/
export const chunterId = 'chunter' as Plugin
export * from './utils'
export default plugin(chunterId, {
icon: {
Chunter: '' as Asset,
@ -131,6 +150,7 @@ export default plugin(chunterId, {
CommentInput: '' as AnyComponent,
DmHeader: '' as AnyComponent,
ChannelView: '' as AnyComponent,
ThreadView: '' as AnyComponent,
CommentsPresenter: '' as AnyComponent
},
class: {
@ -145,6 +165,9 @@ export default plugin(chunterId, {
DirectMessage: '' as Ref<Class<DirectMessage>>,
Reaction: '' as Ref<Class<Reaction>>
},
mixin: {
DirectMessageInput: '' as Ref<Mixin<DirectMessageInput>>
},
space: {
Backlinks: '' as Ref<Space>
},
@ -167,7 +190,8 @@ export default plugin(chunterId, {
DMNotification: '' as Ref<NotificationType>,
MentionNotification: '' as Ref<NotificationType>,
ThreadNotification: '' as Ref<NotificationType>,
ChannelNotification: '' as Ref<NotificationType>
ChannelNotification: '' as Ref<NotificationType>,
DMCreationNotification: '' as Ref<NotificationType>
},
app: {
Chunter: '' as Ref<Doc>

View File

@ -0,0 +1,111 @@
import { deepEqual } from 'fast-equals'
import core, { Class, Data, Doc, Ref, TxOperations } from '@hcengineering/core'
import { PersonAccount } from '@hcengineering/contact'
import chunter, { Backlink, DirectMessage } from '.'
/**
* @public
*/
export async function getDirectChannel (
client: TxOperations,
me: Ref<PersonAccount>,
employeeAccount: Ref<PersonAccount>
): Promise<Ref<DirectMessage>> {
const accIds = [me, employeeAccount].sort()
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
for (const dm of existingDms) {
if (deepEqual(dm.members, accIds)) {
return dm._id
}
}
return await client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: accIds
})
}
function extractBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
message: string,
kids: NodeListOf<ChildNode>
): Array<Data<Backlink>> {
const result: Array<Data<Backlink>> = []
const nodes: Array<NodeListOf<ChildNode>> = [kids]
while (true) {
const nds = nodes.shift()
if (nds === undefined) {
break
}
nds.forEach((kid) => {
if (
kid.nodeType === Node.ELEMENT_NODE &&
(kid as HTMLElement).localName === 'span' &&
(kid as HTMLElement).getAttribute('data-type') === 'reference'
) {
const el = kid as HTMLElement
const ato = el.getAttribute('data-id') as Ref<Doc>
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message: el.parentElement?.innerHTML ?? '',
attachedDocId
})
}
}
nodes.push(kid.childNodes)
})
}
return result
}
/**
* @public
*/
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Array<Data<Backlink>> {
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
}
/**
* @public
*/
export async function createBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
for (const backlink of backlinks) {
const { attachedTo, attachedToClass, collection, ...adata } = backlink
await client.addCollection(
chunter.class.Backlink,
chunter.space.Backlinks,
attachedTo,
attachedToClass,
collection,
adata
)
}
}

View File

@ -19,6 +19,7 @@
"Change": "Change",
"AddedRemoved": "Added/removed",
"YouAddedCollaborators": "You have been added to collaborators",
"YouHaveStartedAConversation": "You have started a conversation",
"ChangeCollaborators": "changed collaborators",
"Activity": "Activity",
"People": "People",

View File

@ -19,6 +19,7 @@
"Change": "Изменено",
"AddedRemoved": "Добавлено/удалено",
"YouAddedCollaborators": "Вы были добавлены как участник",
"YouHaveStartedAConversation": "Вы начали диалог",
"ChangeCollaborators": "изменил(а) участники",
"Activity": "Активность",
"People": "Люди",

View File

@ -42,6 +42,7 @@
"@hcengineering/contact": "^0.6.19",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.8",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/chunter": "^0.6.10",
"@hcengineering/core": "^0.6.27",
"@hcengineering/view": "^0.6.8",

View File

@ -13,18 +13,19 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import activity, { TxViewlet } from '@hcengineering/activity'
import { activityKey, ActivityKey } from '@hcengineering/activity-resources'
import chunter from '@hcengineering/chunter'
import { Employee, getName, PersonAccount } from '@hcengineering/contact'
import { Avatar, employeeByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources'
import core, { Account, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import core, { Account, Class, Doc, getCurrentAccount, Ref } from '@hcengineering/core'
import notification, { DocUpdates } from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { ActionIcon, Button, IconBack, Loading, Scroller } from '@hcengineering/ui'
import { ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { AnySvelteComponent, Button, Loading, Scroller } from '@hcengineering/ui'
import NotificationView from './NotificationView.svelte'
import { getResource } from '@hcengineering/platform'
export let accountId: Ref<Account>
const dispatch = createEventDispatcher()
@ -33,6 +34,7 @@
let _id: Ref<Doc> | undefined
let docs: DocUpdates[] = []
let filteredDocs: DocUpdates[] = []
let loading = true
const me = getCurrentAccount()._id
@ -43,30 +45,8 @@
hidden: false
},
(res) => {
docs = []
for (const doc of res) {
if (doc.txes.length === 0) continue
const txes = doc.txes.filter((p) => p.modifiedBy === accountId)
if (txes.length > 0) {
docs.push({
...doc,
txes
})
}
}
docs = docs
listProvider.update(docs)
if (loading || _id === undefined) {
changeSelected(selected)
} else {
const index = docs.findIndex((p) => p.attachedTo === _id)
if (index === -1) {
changeSelected(selected)
} else {
selected = index
markAsRead(selected)
}
}
docs = res
updateDocs(accountId, true)
loading = false
},
{
@ -76,22 +56,43 @@
}
)
$: updateDocs(accountId)
function updateDocs (accountId: Ref<Account>, forceUpdate = false) {
if (loading && !forceUpdate) {
return
}
const filtered: DocUpdates[] = []
for (const doc of docs) {
if (doc.txes.length === 0) continue
const txes = doc.txes.filter((p) => p.modifiedBy === accountId)
if (txes.length > 0) {
filtered.push({
...doc,
txes
})
}
}
filteredDocs = filtered
}
function markAsRead (index: number) {
if (docs[index] !== undefined) {
docs[index].txes.forEach((p) => (p.isNew = false))
docs[index].txes = docs[index].txes
docs = docs
if (filteredDocs[index] !== undefined) {
filteredDocs[index].txes.forEach((p) => (p.isNew = false))
filteredDocs[index].txes = filteredDocs[index].txes
filteredDocs = filteredDocs
}
}
function changeSelected (index: number) {
if (docs[index] !== undefined) {
listProvider.updateFocus(docs[index])
_id = docs[index]?.attachedTo
dispatch('change', docs[index])
if (filteredDocs[index] !== undefined) {
_id = filteredDocs[index]?.attachedTo
dispatch('change', filteredDocs[index])
markAsRead(index)
} else if (docs.length) {
if (index < docs.length - 1) {
} else if (filteredDocs.length) {
if (index < filteredDocs.length - 1) {
selected++
} else {
selected--
@ -106,17 +107,6 @@
let viewlets: Map<ActivityKey, TxViewlet>
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
if (dir === 'vertical') {
let value = offset + docs.findIndex((p) => p._id === of?._id)
if (value < 0) value = 0
if (docs[value] !== undefined) {
selected = value
changeSelected(selected)
}
}
})
const descriptors = createQuery()
descriptors.query(activity.class.TxViewlet, {}, (result) => {
viewlets = new Map(result.map((r) => [activityKey(r.objectClass, r.txClass), r]))
@ -125,11 +115,12 @@
let selected = 0
let employee: Employee | undefined = undefined
$: newTxes = docs.reduce((acc, cur) => acc + cur.txes.filter((p) => p.isNew).length, 0) // items.length
$: newTxes = filteredDocs.reduce((acc, cur) => acc + cur.txes.filter((p) => p.isNew).length, 0) // items.length
$: account = $personAccountByIdStore.get(accountId as Ref<PersonAccount>)
$: employee = account ? $employeeByIdStore.get(account.person as Ref<Employee>) : undefined
const client = getClient()
const hierarchy = client.getHierarchy()
async function openDM () {
const res = await client.findAll(chunter.class.DirectMessage, { members: accountId })
@ -167,6 +158,15 @@
changeSelected(selected)
}
}
let dmInput: AnySvelteComponent | undefined = undefined
$: dmInputRes = hierarchy.classHierarchyMixin(
chunter.class.DirectMessage as Ref<Class<Doc>>,
chunter.mixin.DirectMessageInput
)?.component
$: if (dmInputRes) {
getResource(dmInputRes).then((res) => (dmInput = res))
}
</script>
<ActionContext
@ -176,15 +176,6 @@
/>
<div class="flex-between header bottom-divider">
<div class="flex-row-center">
<div class="clear-mins flex-no-shrink mr-4">
<ActionIcon
icon={IconBack}
size="medium"
action={() => {
dispatch('close')
}}
/>
</div>
{#if employee}
<Avatar size="smaller" avatar={employee.avatar} />
<span class="font-medium mx-2">{getName(client.getHierarchy(), employee)}</span>
@ -204,21 +195,27 @@
{#if loading}
<Loading />
{:else}
{#each docs as item, i (item._id)}
<NotificationView
value={item}
selected={selected === i}
{viewlets}
on:keydown={onKeydown}
on:click={() => {
selected = i
changeSelected(selected)
}}
/>
{#each filteredDocs as item, i (item._id)}
<div class="with-hover">
<NotificationView
value={item}
selected={false}
{viewlets}
on:keydown={onKeydown}
on:click={() => {
selected = i
changeSelected(selected)
}}
preview
/>
</div>
{/each}
{/if}
</Scroller>
</div>
{#if dmInput && account}
<svelte:component this={dmInput} {account} bind:loading />
{/if}
<style lang="scss">
.header {
@ -239,4 +236,10 @@
background-color: var(--theme-inbox-people-counter-bgcolor);
border-radius: 50%;
}
.with-hover {
&:hover {
background-color: var(--theme-inbox-activitymsg-bgcolor);
}
}
</style>

View File

@ -13,13 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import chunter from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import chunter, { getDirectChannel } from '@hcengineering/chunter'
import { Employee, PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import { AnyComponent, Component, Tabs } from '@hcengineering/ui'
import { AnyComponent, Button, Component, IconAdd, Tabs, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import contact from '@hcengineering/contact'
import { UsersPopup } from '@hcengineering/contact-resources'
import notification from '../plugin'
import Activity from './Activity.svelte'
import EmployeeInbox from './EmployeeInbox.svelte'
@ -84,41 +87,68 @@
}
let selectedTab = 0
const me = getCurrentAccount() as PersonAccount
function openUsersPopup (ev: MouseEvent) {
showPopup(
UsersPopup,
{ _class: contact.mixin.Employee, docQuery: { _id: { $ne: me.person } } },
eventToHTMLElement(ev),
async (employee: Employee) => {
if (employee != null) {
const personAccount = await client.findOne(contact.class.PersonAccount, { person: employee._id })
if (personAccount !== undefined) {
const channel = await getDirectChannel(client, me._id as Ref<PersonAccount>, personAccount._id)
openDM(channel)
}
}
}
)
}
</script>
<div class="flex-row-top h-full">
{#if visibileNav}
<div class="antiPanel-component header border-right aside min-w-100 flex-no-shrink">
{#if selectedEmployee === undefined}
<Tabs
bind:selected={selectedTab}
model={tabs}
on:change={(e) => select(e.detail)}
on:open={(e) => {
selectedEmployee = e.detail
}}
padding={'0 1.75rem'}
size="small"
>
<svelte:fragment slot="rightButtons">
<Tabs
bind:selected={selectedTab}
model={tabs}
on:change={(e) => select(e.detail)}
on:open={(e) => {
selectedEmployee = e.detail
select(undefined)
}}
padding={'0 1.75rem'}
size="small"
>
<svelte:fragment slot="rightButtons">
<div class="flex flex-gap-2">
{#if selectedTab > 0}
<Button label={chunter.string.Message} icon={IconAdd} kind="accented" on:click={openUsersPopup} />
{/if}
<Filter bind:filter />
</svelte:fragment>
</Tabs>
{:else}
<EmployeeInbox
accountId={selectedEmployee}
on:change={(e) => select(e.detail)}
on:dm={(e) => openDM(e.detail)}
on:close={(e) => {
selectedEmployee = undefined
}}
/>
{/if}
</div>
</svelte:fragment>
</Tabs>
</div>
{/if}
<div class="antiPanel-component filled w-full">
{#if component && _id && _class}
<Component is={component} props={{ embedded: true, _id, _class }} />
{#if selectedEmployee !== undefined && component === undefined}
<EmployeeInbox
accountId={selectedEmployee}
on:change={(e) => select(e.detail)}
on:dm={(e) => openDM(e.detail)}
on:close={() => {
selectedEmployee = undefined
}}
/>
{:else if component && _id && _class}
<Component
is={component}
props={{ _id, _class, embedded: selectedTab === 0 }}
on:close={() => select(undefined)}
/>
{/if}
</div>
</div>

View File

@ -22,11 +22,13 @@
import { AnySvelteComponent, TimeSince, getEventPositionElement, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { Menu } from '@hcengineering/view-resources'
import TxView from './TxView.svelte'
export let value: DocUpdates
export let viewlets: Map<ActivityKey, TxViewlet>
export let selected: boolean
export let preview: boolean = false
let doc: Doc | undefined = undefined
let tx: TxCUD<Doc> | undefined = undefined
@ -66,6 +68,21 @@
let div: HTMLDivElement
$: if (selected && div !== undefined) div.focus()
let notificationPreviewPresenter: AnySvelteComponent | undefined = undefined
$: notificationPreviewPresenterRes = hierarchy.classHierarchyMixin(
value.attachedToClass,
notification.mixin.NotificationPreview
)?.presenter
$: if (notificationPreviewPresenterRes) {
getResource(notificationPreviewPresenterRes).then((res) => (notificationPreviewPresenter = res))
}
let object: Doc | undefined
const objQuery = createQuery()
$: objQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[object] = res
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -85,14 +102,21 @@
<div class="counter float">{newTxes}</div>
{/if}
</div>
<div class="flex-between flex-baseline mt-3">
{#if tx}
<TxView {tx} {viewlets} objectId={value.attachedTo} />
{/if}
<div class="time">
<TimeSince value={tx?.modifiedOn} />
{#if preview && object && notificationPreviewPresenter !== undefined}
<div class="mt-2">
<svelte:component this={notificationPreviewPresenter} {object} {newTxes} />
</div>
</div>
{/if}
{#if !preview || notificationPreviewPresenter === undefined}
<div class="flex-between flex-baseline mt-3">
{#if tx}
<TxView {tx} {viewlets} objectId={value.attachedTo} />
{/if}
<div class="time">
<TimeSince value={tx?.modifiedOn} />
</div>
</div>
{/if}
</div>
</div>
{/if}

View File

@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import activity, { TxViewlet } from '@hcengineering/activity'
import { activityKey, ActivityKey } from '@hcengineering/activity-resources'
import { PersonAccount } from '@hcengineering/contact'
@ -21,7 +22,7 @@
import notification, { DocUpdates } from '@hcengineering/notification'
import { createQuery } from '@hcengineering/presentation'
import { Loading, Scroller } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import PeopleNotificationView from './PeopleNotificationsView.svelte'
export let filter: 'all' | 'read' | 'unread' = 'all'
@ -40,9 +41,9 @@
user: getCurrentAccount()._id,
hidden: false
},
(res) => {
async (res) => {
docs = res
getFiltered(docs, filter)
await getFiltered(docs, filter)
loading = false
},
{
@ -52,7 +53,7 @@
}
)
function getFiltered (docs: DocUpdates[], filter: 'all' | 'read' | 'unread'): void {
async function getFiltered (docs: DocUpdates[], filter: 'all' | 'read' | 'unread'): Promise<void> {
const filtered: DocUpdates[] = []
for (const doc of docs) {
if (doc.txes.length === 0) continue
@ -169,7 +170,7 @@
{viewlets}
on:keydown={onKeydown}
on:open
on:click={() => {
on:open={() => {
selected = i
}}
/>

View File

@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { TxViewlet } from '@hcengineering/activity'
import { ActivityKey } from '@hcengineering/activity-resources'
import { PersonAccount, getName } from '@hcengineering/contact'
@ -23,7 +24,7 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { ActionIcon, AnySvelteComponent, Label, TimeSince } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import TxView from './TxView.svelte'
import ArrowRight from './icons/ArrowRight.svelte'
@ -71,7 +72,7 @@
let div: HTMLDivElement
$: if (selected && div !== undefined) div.focus()
$: if (selected && div != null) div.focus()
</script>
{#if doc}

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { TxCreateDoc } from '@hcengineering/core'
import { Label } from '@hcengineering/ui'
import { DirectMessage } from '@hcengineering/chunter'
import notification from '../../plugin'
export let tx: TxCreateDoc<DirectMessage>
export let value: DirectMessage
</script>
<Label label={notification.string.YouHaveStartedAConversation} />

View File

@ -19,6 +19,7 @@ import Inbox from './components/Inbox.svelte'
import NotificationSettings from './components/NotificationSettings.svelte'
import NotificationPresenter from './components/NotificationPresenter.svelte'
import TxCollaboratorsChange from './components/activity/TxCollaboratorsChange.svelte'
import TxDmCreation from './components/activity/TxDmCreation.svelte'
import { NotificationClientImpl, hasntNotifications, hide, markAsUnread, unsubscribe } from './utils'
export * from './utils'
@ -32,7 +33,8 @@ export default async (): Promise<Resources> => ({
NotificationSettings
},
activity: {
TxCollaboratorsChange
TxCollaboratorsChange,
TxDmCreation
},
function: {
GetNotificationClient: NotificationClientImpl.getClient,

View File

@ -30,6 +30,7 @@ export default mergeIds(notificationId, notification, {
Change: '' as IntlString,
AddedRemoved: '' as IntlString,
YouAddedCollaborators: '' as IntlString,
YouHaveStartedAConversation: '' as IntlString,
ChangeCollaborators: '' as IntlString,
Activity: '' as IntlString,
People: '' as IntlString,

View File

@ -189,6 +189,13 @@ export interface NotificationClient {
forceRead: (_id: Ref<Doc>, _class: Ref<Class<Doc>>, space: Ref<Space>) => Promise<void>
}
/**
* @public
*/
export interface NotificationPreview extends Class<Doc> {
presenter: AnyComponent
}
/**
* @public
*/
@ -201,7 +208,8 @@ const notification = plugin(notificationId, {
mixin: {
ClassCollaborators: '' as Ref<Mixin<ClassCollaborators>>,
Collaborators: '' as Ref<Mixin<Collaborators>>,
NotificationObjectPresenter: '' as Ref<Mixin<NotificationObjectPresenter>>
NotificationObjectPresenter: '' as Ref<Mixin<NotificationObjectPresenter>>,
NotificationPreview: '' as Ref<Mixin<NotificationPreview>>
},
class: {
Notification: '' as Ref<Class<Notification>>,

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import chunter from '@hcengineering/chunter'
import chunter, { createBacklinks } from '@hcengineering/chunter'
import { Employee } from '@hcengineering/contact'
import core, { Account, AttachedData, Doc, fillDefaults, generateId, Ref, SortingOrder } from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform'
@ -70,7 +70,6 @@
import MilestoneSelector from './milestones/MilestoneSelector.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import { createBacklinks } from '@hcengineering/chunter-resources'
import ProjectPresenter from './projects/ProjectPresenter.svelte'
export let space: Ref<Project>

View File

@ -15,6 +15,7 @@
<script lang="ts">
import { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createBacklinks } from '@hcengineering/chunter'
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import { Milestone, MilestoneStatus, Project } from '@hcengineering/tracker'
import ui, { DatePresenter, EditBox } from '@hcengineering/ui'
@ -22,7 +23,6 @@
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import MilestoneStatusEditor from './MilestoneStatusEditor.svelte'
import { createBacklinks } from '@hcengineering/chunter-resources'
import ProjectPresenter from '../projects/ProjectPresenter.svelte'
export let space: Ref<Project>

View File

@ -457,7 +457,7 @@
specialComponent = getSpecialComponent(spaceSpecial)
if (specialComponent !== undefined) {
currentSpecial = spaceSpecial
} else if (navigatorModel?.aside !== undefined) {
} else if (navigatorModel?.aside !== undefined || currentApplication?.aside !== undefined) {
asideId = spaceSpecial
}
}
@ -760,17 +760,20 @@
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}
</div>
{#if asideId && navigatorModel?.aside !== undefined}
<div class="splitter" class:hovered={isResizing} on:mousedown={startResize} />
<div
class="antiPanel-component antiComponent aside"
use:resizeObserver={(element) => {
asideWidth = element.clientWidth
}}
bind:this={aside}
>
<Component is={navigatorModel.aside} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{#if asideId}
{@const asideComponent = navigatorModel?.aside ?? currentApplication?.aside}
{#if asideComponent !== undefined}
<div class="splitter" class:hovered={isResizing} on:mousedown={startResize} />
<div
class="antiPanel-component antiComponent aside"
use:resizeObserver={(element) => {
asideWidth = element.clientWidth
}}
bind:this={aside}
>
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{/if}
{/if}
</div>
</div>

View File

@ -31,6 +31,7 @@ export interface Application extends Doc {
// Also attached ApplicationNavModel will be joined after this one main.
navigatorModel?: NavigatorModel
aside?: AnyComponent
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>

View File

@ -13,7 +13,15 @@
// limitations under the License.
//
import chunter, { Backlink, chunterId, ChunterSpace, Comment, Message, ThreadMessage } from '@hcengineering/chunter'
import chunter, {
Backlink,
chunterId,
ChunterSpace,
Comment,
DirectMessage,
Message,
ThreadMessage
} from '@hcengineering/chunter'
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
@ -36,7 +44,7 @@ import core, {
import notification, { Collaborators, NotificationType } from '@hcengineering/notification'
import { getMetadata } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { getDocCollaborators, getMixinTx } from '@hcengineering/server-notification-resources'
import { pushNotification, getDocCollaborators, getMixinTx } from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench'
/**
@ -206,6 +214,34 @@ export async function ChunterTrigger (tx: Tx, control: TriggerControl): Promise<
return res.flat()
}
/**
* @public
*/
export async function OnDmCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const ptx = tx as TxCreateDoc<DirectMessage>
const res: Tx[] = []
if (tx.createdBy == null) return []
const dm = TxProcessor.createDoc2Doc(ptx)
if (dm.members.length > 2) return []
let dmWithPerson: Ref<Account> | undefined
for (const person of dm.members) {
if (person !== tx.createdBy) {
dmWithPerson = person
break
}
}
if (dmWithPerson == null) return []
pushNotification(control, res, tx.createdBy, dm, ptx, [], dmWithPerson)
return res
}
/**
* @public
*/
@ -272,7 +308,8 @@ export async function IsChannelMessage (
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
ChunterTrigger
ChunterTrigger,
OnDmCreate
},
function: {
CommentRemove,

View File

@ -29,7 +29,8 @@ export const serverChunterId = 'server-chunter' as Plugin
*/
export default plugin(serverChunterId, {
trigger: {
ChunterTrigger: '' as Resource<TriggerFunc>
ChunterTrigger: '' as Resource<TriggerFunc>,
OnDmCreate: '' as Resource<TriggerFunc>
},
function: {
CommentRemove: '' as Resource<

View File

@ -432,13 +432,17 @@ async function isShouldNotify (
}
}
function pushNotification (
/**
* @public
*/
export function pushNotification (
control: TriggerControl,
res: Tx[],
target: Ref<Account>,
object: Doc,
originTx: TxCUD<Doc>,
docUpdates: DocUpdates[]
docUpdates: DocUpdates[],
modifiedBy?: Ref<Account>
): void {
const current = docUpdates.find((p) => p.user === target)
if (current === undefined) {
@ -449,14 +453,26 @@ function pushNotification (
attachedToClass: object._class,
hidden: false,
lastTxTime: originTx.modifiedOn,
txes: [{ _id: originTx._id, modifiedOn: originTx.modifiedOn, modifiedBy: originTx.modifiedBy, isNew: true }]
txes: [
{
_id: originTx._id,
modifiedOn: originTx.modifiedOn,
modifiedBy: modifiedBy ?? originTx.modifiedBy,
isNew: true
}
]
})
)
} else {
res.push(
control.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
$push: {
txes: { _id: originTx._id, modifiedOn: originTx.modifiedOn, modifiedBy: originTx.modifiedBy, isNew: true }
txes: {
_id: originTx._id,
modifiedOn: originTx.modifiedOn,
modifiedBy: modifiedBy ?? originTx.modifiedBy,
isNew: true
}
}
})
)