Chunter last views (#1273)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-05 12:19:18 +06:00 committed by GitHub
parent f13353c9ec
commit ed1c29573f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 317 additions and 108 deletions

View File

@ -33,4 +33,8 @@ export function createModel (builder: Builder): void {
builder.mixin<Class<Doc>, ObjectDDParticipant>(chunter.class.Comment, core.class.Class, serverCore.mixin.ObjectDDParticipant, { builder.mixin<Class<Doc>, ObjectDDParticipant>(chunter.class.Comment, core.class.Class, serverCore.mixin.ObjectDDParticipant, {
collectDocs: serverChunter.function.CommentRemove collectDocs: serverChunter.function.CommentRemove
}) })
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.CommentCreate
})
} }

View File

@ -304,6 +304,7 @@ p:last-child { margin-block-end: 0; }
.mb-2 { margin-bottom: .5rem; } .mb-2 { margin-bottom: .5rem; }
.mb-3 { margin-bottom: .75rem; } .mb-3 { margin-bottom: .75rem; }
.mb-4 { margin-bottom: 1rem; } .mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mx-1 { margin: 0 .25rem; } .mx-1 { margin: 0 .25rem; }
.mx-2 { margin: 0 .5rem; } .mx-2 { margin: 0 .5rem; }
.mx-3 { margin: 0 .75rem; } .mx-3 { margin: 0 .75rem; }

View File

@ -24,6 +24,7 @@
"Replies": "Replies", "Replies": "Replies",
"LastReply": "Last reply", "LastReply": "Last reply",
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}", "RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Thread": "Thread" "Thread": "Thread",
"New": "New"
} }
} }

View File

@ -24,6 +24,7 @@
"Replies": "Ответы", "Replies": "Ответы",
"LastReply": "Последний ответ", "LastReply": "Последний ответ",
"RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}", "RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}",
"Thread": "Обсуждение" "Thread": "Обсуждение",
"New": "Новое"
} }
} }

View File

@ -41,6 +41,7 @@
"@anticrm/text-editor": "~0.6.0", "@anticrm/text-editor": "~0.6.0",
"@anticrm/contact": "~0.6.5", "@anticrm/contact": "~0.6.5",
"@anticrm/contact-resources": "~0.6.0", "@anticrm/contact-resources": "~0.6.0",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/attachment": "~0.6.1", "@anticrm/attachment": "~0.6.1",
"@anticrm/attachment-resources": "~0.6.0", "@anticrm/attachment-resources": "~0.6.0",
"@anticrm/view-resources": "~0.6.0", "@anticrm/view-resources": "~0.6.0",

View File

@ -14,20 +14,37 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment'
import type { Message } from '@anticrm/chunter' import type { Message } from '@anticrm/chunter'
import contact,{ Employee } from '@anticrm/contact' import contact,{ Employee } from '@anticrm/contact'
import { Ref,Space } from '@anticrm/core' import core, { Doc,Ref,Space,Timestamp, WithLookup } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { createQuery } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
import { afterUpdate, beforeUpdate } from 'svelte'
import chunter from '../plugin' import chunter from '../plugin'
import ChannelSeparator from './ChannelSeparator.svelte'
import MessageComponent from './Message.svelte' import MessageComponent from './Message.svelte'
export let space: Ref<Space> | undefined export let space: Ref<Space> | undefined
let messages: Message[] | undefined let div: HTMLDivElement | undefined
let autoscroll: boolean = false
beforeUpdate(() => {
autoscroll = div !== undefined && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20)
})
afterUpdate(() => {
if (div && autoscroll) div.scrollTo(0, div.scrollHeight)
})
let messages: WithLookup<Message>[] | undefined
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>() let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
const query = createQuery() const query = createQuery()
const employeeQuery = createQuery() const employeeQuery = createQuery()
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
employeeQuery.query(contact.class.Employee, { }, (res) => employees = new Map(res.map((r) => { return [r._id, r] }))) employeeQuery.query(contact.class.Employee, { }, (res) => employees = new Map(res.map((r) => { return [r._id, r] })))
$: updateQuery(space) $: updateQuery(space)
@ -42,14 +59,37 @@
space space
}, (res) => { }, (res) => {
messages = res messages = res
newMessagesPos = newMessagesStart(messages)
notificationClient.updateLastView(space, chunter.class.Channel)
}, {
lookup: {
_id: { attachments: attachment.class.Attachment },
createBy: core.class.Account
}
}) })
} }
function newMessagesStart (messages: Message[]): number {
if (space === undefined) return -1
const lastView = $lastViews.get(space)
if (lastView === undefined) return -1
for (let index = 0; index < messages.length; index++) {
const message = messages[index]
if (message.createOn > lastView) return index
}
return -1
}
let newMessagesPos: number = -1
</script> </script>
<div class="flex-col container"> <div class="flex-col vScroll container" bind:this={div}>
{#if messages} {#if messages}
{#each messages as message} {#each messages as message, i (message._id)}
{#if newMessagesPos === i}
<ChannelSeparator title={chunter.string.New} line reverse isNew />
{/if}
<MessageComponent {message} {employees} on:openThread /> <MessageComponent {message} {employees} on:openThread />
{/each} {/each}
{/if} {/if}
@ -57,6 +97,7 @@
<style lang="scss"> <style lang="scss">
.container { .container {
flex-shrink: 0; margin: 1rem 1rem 0;
padding: 1.5rem 1.5rem 0px;
} }
</style> </style>

View File

@ -20,12 +20,13 @@
export let title: IntlString export let title: IntlString
export let line: boolean = false export let line: boolean = false
export let params: any = undefined export let params: any = undefined
export let reverse: boolean = false
export let isNew: boolean = false
</script> </script>
<div class="w-full flex-center whitespace-nowrap mb-4"> <div class="w-full text-sm flex-center whitespace-nowrap mb-6" class:flex-reverse={reverse} class:new={isNew} >
<Label label={title} {params} /> <Label label={title} {params} />
<div class="ml-4" class:line={line} ></div> <div class:ml-4={!reverse} class:mr-4={reverse} class:line={line} ></div>
</div> </div>
<style lang="scss"> <style lang="scss">
@ -34,5 +35,12 @@
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: var(--theme-chat-divider); background-color: var(--theme-chat-divider);
}
.new {
.line {
background-color: var(--highlight-red);
}
color: var(--highlight-red);
} }
</style> </style>

View File

@ -17,6 +17,7 @@
import { AttachmentRefInput } from '@anticrm/attachment-resources' import { AttachmentRefInput } from '@anticrm/attachment-resources'
import { Message } from '@anticrm/chunter' import { Message } from '@anticrm/chunter'
import { generateId,getCurrentAccount,Ref,Space, TxFactory } from '@anticrm/core' import { generateId,getCurrentAccount,Ref,Space, TxFactory } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { getCurrentLocation,navigate } from '@anticrm/ui' import { getCurrentLocation,navigate } from '@anticrm/ui'
import { createBacklinks } from '../backlinks' import { createBacklinks } from '../backlinks'
@ -28,6 +29,7 @@
const client = getClient() const client = getClient()
const _class = chunter.class.Message const _class = chunter.class.Message
let _id = generateId() as Ref<Message> let _id = generateId() as Ref<Message>
const notificationClient = NotificationClientImpl.getClient()
async function onMessage (event: CustomEvent) { async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail const { message, attachments } = event.detail
@ -40,10 +42,12 @@
attachments attachments
}, _id) }, _id)
tx.attributes.createOn = tx.modifiedOn tx.attributes.createOn = tx.modifiedOn
await notificationClient.updateLastView(space, chunter.class.Channel, tx.modifiedOn, true)
await client.tx(tx) await client.tx(tx)
// Create an backlink to document // Create an backlink to document
await createBacklinks(client, space, chunter.class.Channel, _id, message) await createBacklinks(client, space, chunter.class.Channel, _id, message)
_id = generateId() _id = generateId()
} }
@ -55,22 +59,12 @@
</script> </script>
<div class="msg-board"> <Channel {space} on:openThread={(e) => { openThread(e.detail) }} />
<Channel {space} on:openThread={(e) => { openThread(e.detail) }} />
</div>
<div class="reference"> <div class="reference">
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/> <AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/>
</div> </div>
<style lang="scss"> <style lang="scss">
.msg-board {
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 1rem 1rem 0;
padding: 1.5rem 1.5rem 0px;
overflow: auto;
}
.reference { .reference {
margin: 1.25rem 2.5rem; margin: 1.25rem 2.5rem;
} }

View File

@ -0,0 +1,82 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 { Timestamp } from "@anticrm/core"
export let value: Timestamp
export let line: boolean = false
const current = new Date()
const target = new Date(value)
let options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'long'
}
if (current.getFullYear() !== target.getFullYear()) {
options = {
...options,
year: '2-digit'
}
}
</script>
<div class="flex-center container" class:line={line}>
<div class="title">{new Intl.DateTimeFormat('default', options).format(value)}</div>
</div>
<style lang="scss">
.container {
width: 100%;
height: 1.75rem;
margin-bottom: 2rem;
.title {
position: relative;
padding: .375rem .75rem;
font-weight: 600;
font-size: .75rem;
letter-spacing: .5;
text-transform: uppercase;
color: var(--theme-content-trans-color);
z-index: 1;
&::before {
position: absolute;
content: '';
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 1.25rem;
background-color: var(--theme-chat-divider);
z-index: -1;
}
}
&.line {
position: relative;
width: 100%;
&::before {
position: absolute;
content: '';
top: 50%;
left: 0;
width: 100%;
height: 1px;
background-color: var(--theme-chat-divider);
}
}
}
</style>

View File

@ -14,10 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentDocList } from '@anticrm/attachment-resources' import { Attachment } from '@anticrm/attachment'
import { AttachmentList } from '@anticrm/attachment-resources'
import type { Message } from '@anticrm/chunter' import type { Message } from '@anticrm/chunter'
import contact,{ Employee,EmployeeAccount,formatName } from '@anticrm/contact' import { Employee,EmployeeAccount,formatName } from '@anticrm/contact'
import { Account,Ref } from '@anticrm/core' import { Ref,WithLookup } from '@anticrm/core'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import { Avatar,getClient,MessageViewer } from '@anticrm/presentation' import { Avatar,getClient,MessageViewer } from '@anticrm/presentation'
import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui' import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui'
@ -32,10 +33,13 @@
import Reactions from './Reactions.svelte' import Reactions from './Reactions.svelte'
import Replies from './Replies.svelte' import Replies from './Replies.svelte'
export let message: Message export let message: WithLookup<Message>
export let employees: Map<Ref<Employee>, Employee> export let employees: Map<Ref<Employee>, Employee>
export let thread: boolean = false export let thread: boolean = false
$: employee = getEmployee(message)
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
const client = getClient() const client = getClient()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -62,10 +66,11 @@
) )
} }
async function getEmployee (createdBy: Ref<Account>): Promise<Employee | undefined> { function getEmployee (message: WithLookup<Message>): Employee | undefined {
const account = await client.findOne(contact.class.EmployeeAccount, { _id: createdBy as Ref<EmployeeAccount> }) const employee = (message.$lookup?.createBy as EmployeeAccount).employee
if (account === undefined) return if (employee !== undefined) {
return employees.get(account.employee) return employees.get(employee)
}
} }
function openThread () { function openThread () {
@ -74,7 +79,6 @@
</script> </script>
<div class="container"> <div class="container">
{#await getEmployee(message.createBy) then employee}
<div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div> <div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div>
<div class="message"> <div class="message">
<div class="header"> <div class="header">
@ -82,7 +86,7 @@
<span>{getTime(message.createOn)}</span> <span>{getTime(message.createOn)}</span>
</div> </div>
<div class="text"><MessageViewer message={message.content}/></div> <div class="text"><MessageViewer message={message.content}/></div>
{#if message.attachments}<div class="attachments"><AttachmentDocList value={message} /></div>{/if} {#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
{#if (reactions || message.replies)} {#if (reactions || message.replies)}
<div class="footer flex-col"> <div class="footer flex-col">
<div>{#if reactions}<Reactions/>{/if}</div> <div>{#if reactions}<Reactions/>{/if}</div>
@ -92,7 +96,6 @@
</div> </div>
{/if} {/if}
</div> </div>
{/await}
<div class="buttons"> <div class="buttons">
<div class="tool"><ActionIcon icon={IconMoreH} size={'medium'} action={(e) => { showMenu(e) }}/></div> <div class="tool"><ActionIcon icon={IconMoreH} size={'medium'} action={(e) => { showMenu(e) }}/></div>
{#if !thread} {#if !thread}

View File

@ -14,10 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentDocList } from '@anticrm/attachment-resources' import { Attachment } from '@anticrm/attachment'
import { AttachmentList } from '@anticrm/attachment-resources'
import type { Comment } from '@anticrm/chunter' import type { Comment } from '@anticrm/chunter'
import contact,{ Employee,EmployeeAccount,formatName } from '@anticrm/contact' import { Employee,EmployeeAccount,formatName } from '@anticrm/contact'
import { Account,Ref } from '@anticrm/core' import { Ref,WithLookup } from '@anticrm/core'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import { Avatar,getClient,MessageViewer } from '@anticrm/presentation' import { Avatar,getClient,MessageViewer } from '@anticrm/presentation'
import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui' import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui'
@ -29,9 +30,11 @@
import Emoji from './icons/Emoji.svelte' import Emoji from './icons/Emoji.svelte'
import Reactions from './Reactions.svelte' import Reactions from './Reactions.svelte'
export let comment: Comment export let comment: WithLookup<Comment>
export let employees: Map<Ref<Employee>, Employee> export let employees: Map<Ref<Employee>, Employee>
$: attachments = (comment.$lookup?.attachments ?? []) as Attachment[]
const client = getClient() const client = getClient()
let reactions: boolean = false let reactions: boolean = false
@ -56,15 +59,17 @@
) )
} }
async function getEmployee (createdBy: Ref<Account>): Promise<Employee | undefined> { $: employee = getEmployee(comment)
const account = await client.findOne(contact.class.EmployeeAccount, { _id: createdBy as Ref<EmployeeAccount> })
if (account === undefined) return function getEmployee (comment: WithLookup<Comment>): Employee | undefined {
return employees.get(account.employee) const employee = (comment.$lookup?.modifiedBy as EmployeeAccount)?.employee
if (employee !== undefined) {
return employees.get(employee)
}
} }
</script> </script>
<div class="container"> <div class="container">
{#await getEmployee(comment.modifiedBy) then employee}
<div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div> <div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div>
<div class="message"> <div class="message">
<div class="header"> <div class="header">
@ -72,14 +77,13 @@
<span>{getTime(comment.modifiedOn)}</span> <span>{getTime(comment.modifiedOn)}</span>
</div> </div>
<div class="text"><MessageViewer message={comment.message}/></div> <div class="text"><MessageViewer message={comment.message}/></div>
{#if comment.attachments}<div class="attachments"><AttachmentDocList value={comment} /></div>{/if} {#if comment.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if}
{#if reactions} {#if reactions}
<div class="footer"> <div class="footer">
<div><Reactions/></div> <div><Reactions/></div>
</div> </div>
{/if} {/if}
</div> </div>
{/await}
<div class="buttons"> <div class="buttons">
<div class="tool"><ActionIcon icon={IconMoreH} size={'medium'} action={(e) => { showMenu(e) }}/></div> <div class="tool"><ActionIcon icon={IconMoreH} size={'medium'} action={(e) => { showMenu(e) }}/></div>
<div class="tool"><ActionIcon icon={Bookmark} size={'medium'}/></div> <div class="tool"><ActionIcon icon={Bookmark} size={'medium'}/></div>

View File

@ -16,11 +16,12 @@
import attachment from '@anticrm/attachment' import attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources' import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Comment,Message } from '@anticrm/chunter' import type { Comment,Message } from '@anticrm/chunter'
import contact,{ Employee, EmployeeAccount } from '@anticrm/contact' import contact,{ Employee,EmployeeAccount } from '@anticrm/contact'
import { Class,generateId,getCurrentAccount,Lookup,Ref, Space } from '@anticrm/core' import core,{ generateId,getCurrentAccount,Ref,Space,TxFactory } from '@anticrm/core'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { createQuery,getClient } from '@anticrm/presentation' import { createQuery,getClient } from '@anticrm/presentation'
import { IconClose,Label } from '@anticrm/ui' import { IconClose,Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { afterUpdate,beforeUpdate,createEventDispatcher } from 'svelte'
import { createBacklinks } from '../backlinks' import { createBacklinks } from '../backlinks'
import chunter from '../plugin' import chunter from '../plugin'
import ChannelSeparator from './ChannelSeparator.svelte' import ChannelSeparator from './ChannelSeparator.svelte'
@ -37,8 +38,23 @@
let message: Message | undefined let message: Message | undefined
let commentId = generateId() let commentId = generateId()
let div: HTMLDivElement | undefined
let autoscroll: boolean = false
beforeUpdate(() => {
autoscroll = div !== undefined && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20)
})
afterUpdate(() => {
if (div && autoscroll) div.scrollTo(0, div.scrollHeight)
})
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
const lookup = { const lookup = {
_id: { attachments: attachment.class.Attachment } _id: { attachments: attachment.class.Attachment },
modifiedBy: core.class.Account
} }
$: updateQueries(_id) $: updateQueries(_id)
@ -47,12 +63,19 @@
messageQuery.query(chunter.class.Message, { messageQuery.query(chunter.class.Message, {
_id: id _id: id
}, (res) => message = res[0], { }, (res) => message = res[0], {
lookup lookup: {
_id: { attachments: attachment.class.Attachment },
createBy: core.class.Account
}
}) })
query.query(chunter.class.Comment, { query.query(chunter.class.Comment, {
attachedTo: id attachedTo: id
}, (res) => comments = res, { }, (res) => {
comments = res
newMessagesPos = newMessagesStart(comments)
notificationClient.updateLastView(id, chunter.class.Message)
}, {
lookup lookup
}) })
} }
@ -64,8 +87,9 @@
async function onMessage (event: CustomEvent) { async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail const { message, attachments } = event.detail
const employee = (getCurrentAccount() as EmployeeAccount).employee const me = getCurrentAccount()._id
await client.createDoc(chunter.class.Comment, space, { const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc(chunter.class.Comment, space, {
attachedTo: _id, attachedTo: _id,
attachedToClass: chunter.class.Message, attachedToClass: chunter.class.Message,
collection: 'replies', collection: 'replies',
@ -73,39 +97,49 @@
attachments attachments
}, commentId) }, commentId)
await client.updateDoc(chunter.class.Message, space, _id, { await notificationClient.updateLastView(_id, chunter.class.Message, tx.modifiedOn, true)
$push: { replies: employee }, await client.tx(tx)
lastReply: new Date().getTime()
})
// Create an backlink to document // Create an backlink to document
await createBacklinks(client, space, chunter.class.Channel, commentId, message) await createBacklinks(client, space, chunter.class.Channel, commentId, message)
commentId = generateId() commentId = generateId()
} }
let comments: Comment[] = [] let comments: Comment[] = []
function newMessagesStart (comments: Comment[]): number {
const lastView = $lastViews.get(_id)
if (lastView === undefined) return -1
for (let index = 0; index < comments.length; index++) {
const comment = comments[index]
if (comment.modifiedOn > lastView) return index
}
return -1
}
let newMessagesPos: number = -1
</script> </script>
<div class="header"> <div class="header">
<div class="title"><Label label={chunter.string.Thread} /></div> <div class="title"><Label label={chunter.string.Thread} /></div>
<div class="tool" on:click={() => { dispatch('close') }}><IconClose size='medium' /></div> <div class="tool" on:click={() => { dispatch('close') }}><IconClose size='medium' /></div>
</div> </div>
<div class="h-full flex-col"> <div class="flex-col vScroll content" bind:this={div}>
<div class="content">
{#if message} {#if message}
<div class="flex-col">
<MsgView {message} {employees} thread /> <MsgView {message} {employees} thread />
{#if comments.length} {#if comments.length}
<ChannelSeparator title={chunter.string.RepliesCount} line params={{ replies: message.replies?.length }} /> <ChannelSeparator title={chunter.string.RepliesCount} line params={{ replies: comments.length }} />
{/if}
{#each comments as comment, i (comment._id)}
{#if newMessagesPos === i}
<ChannelSeparator title={chunter.string.New} line reverse isNew />
{/if} {/if}
{#each comments as comment}
<ThreadComment {comment} {employees} /> <ThreadComment {comment} {employees} />
{/each} {/each}
</div>
{/if} {/if}
</div> </div>
<div class="ref-input"> <div class="ref-input">
<AttachmentRefInput {space} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage}/> <AttachmentRefInput {space} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage}/>
</div>
</div> </div>
<style lang="scss"> <style lang="scss">
@ -134,9 +168,6 @@
} }
} }
.content { .content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 1rem 1rem 0px; margin: 1rem 1rem 0px;
padding: 1.5rem 1.5rem 0px; padding: 1.5rem 1.5rem 0px;
} }

View File

@ -36,6 +36,7 @@ export default mergeIds(chunterId, chunter, {
Replies: '' as IntlString, Replies: '' as IntlString,
Thread: '' as IntlString, Thread: '' as IntlString,
RepliesCount: '' as IntlString, RepliesCount: '' as IntlString,
LastReply: '' as IntlString LastReply: '' as IntlString,
New: '' as IntlString
} }
}) })

View File

@ -391,9 +391,7 @@
{/if} {/if}
</div> </div>
{#if asideId && navigatorModel?.aside !== undefined} {#if asideId && navigatorModel?.aside !== undefined}
<div class="antiPanel-component indent antiComponent filled"> <div class="antiPanel-component antiComponent border-left"><Component is={navigatorModel.aside} props={{ currentSpace, _id: asideId }} on:close={closeAside} /></div>
<Component is={navigatorModel.aside} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{/if} {/if}
</div> </div>
<PanelInstance {contentPanel} /> <PanelInstance {contentPanel} />

View File

@ -29,6 +29,7 @@
"@anticrm/core": "~0.6.16", "@anticrm/core": "~0.6.16",
"@anticrm/platform": "~0.6.5", "@anticrm/platform": "~0.6.5",
"@anticrm/server-core": "~0.6.1", "@anticrm/server-core": "~0.6.1",
"@anticrm/contact": "~0.6.5",
"@anticrm/chunter": "~0.6.1", "@anticrm/chunter": "~0.6.1",
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0",
"@anticrm/login": "~0.6.1", "@anticrm/login": "~0.6.1",

View File

@ -13,10 +13,12 @@
// limitations under the License. // limitations under the License.
// //
import chunter, { Comment, Channel } from '@anticrm/chunter' import chunter, { Channel, Comment, Message } from '@anticrm/chunter'
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@anticrm/core' import { EmployeeAccount } from '@anticrm/contact'
import core, { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref, Tx, TxCreateDoc, TxProcessor, TxUpdateDoc } from '@anticrm/core'
import login from '@anticrm/login' import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform' import { getMetadata } from '@anticrm/platform'
import { TriggerControl } from '@anticrm/server-core'
import workbench from '@anticrm/workbench' import workbench from '@anticrm/workbench'
/** /**
@ -49,8 +51,40 @@ export async function CommentRemove (doc: Doc, hiearachy: Hierarchy, findAll: <T
return result return result
} }
/**
* @public
*/
export async function CommentCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
if (tx._class !== core.class.TxCreateDoc) return []
const doc = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Doc>)
if (!hierarchy.isDerived(doc._class, chunter.class.Comment)) {
return []
}
const comment = doc as Comment
if (!hierarchy.isDerived(comment.attachedToClass, chunter.class.Message)) {
return []
}
const lastReplyTx = control.txFactory.createTxUpdateDoc<Message>(chunter.class.Message, comment.space, comment.attachedTo as Ref<Message>, {
lastReply: tx.modifiedOn
})
const employee = control.modelDb.getObject(tx.modifiedBy) as EmployeeAccount
const employeeTx = control.txFactory.createTxUpdateDoc<Message>(chunter.class.Message, comment.space, comment.attachedTo as Ref<Message>, {
$push: { replies: employee.employee }
})
const result: TxUpdateDoc<Message>[] = []
result.push(lastReplyTx)
result.push(employeeTx)
return result
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({
trigger: {
CommentCreate
},
function: { function: {
CommentRemove, CommentRemove,
ChannelHTMLPresenter: channelHTMLPresenter, ChannelHTMLPresenter: channelHTMLPresenter,

View File

@ -16,6 +16,7 @@
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@anticrm/core' import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@anticrm/core'
import type { Plugin, Resource } from '@anticrm/platform' import type { Plugin, Resource } from '@anticrm/platform'
import { plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform'
import { TriggerFunc } from '@anticrm/server-core'
/** /**
* @public * @public
@ -26,6 +27,9 @@ export const serverChunterId = 'server-chunter' as Plugin
* @public * @public
*/ */
export default plugin(serverChunterId, { export default plugin(serverChunterId, {
trigger: {
CommentCreate: '' as Resource<TriggerFunc>
},
function: { function: {
CommentRemove: '' as Resource<(doc: Doc, hiearachy: Hierarchy, findAll: <T extends Doc> (clazz: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>) => Promise<FindResult<T>>) => Promise<Doc[]>>, CommentRemove: '' as Resource<(doc: Doc, hiearachy: Hierarchy, findAll: <T extends Doc> (clazz: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>) => Promise<FindResult<T>>) => Promise<Doc[]>>,
ChannelHTMLPresenter: '' as Resource<(doc: Doc) => string>, ChannelHTMLPresenter: '' as Resource<(doc: Doc) => string>,