Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-02 10:08:37 +06:00 committed by GitHub
parent b6de3da74a
commit 48b3f3facc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 636 additions and 177 deletions

View File

@ -30,6 +30,7 @@
"@anticrm/ui": "~0.6.0",
"@anticrm/view": "~0.6.0",
"@anticrm/model-attachment": "~0.6.0",
"@anticrm/contact": "~0.6.5",
"@anticrm/chunter": "~0.6.1",
"@anticrm/chunter-resources": "~0.6.0",
"@anticrm/platform": "~0.6.5",

View File

@ -15,14 +15,15 @@
import activity from '@anticrm/activity'
import type { Backlink, Channel, Comment, Message } from '@anticrm/chunter'
import type { Class, Doc, Domain, Ref } from '@anticrm/core'
import type { Account, Class, Doc, Domain, Ref, Timestamp } from '@anticrm/core'
import { IndexKind } from '@anticrm/core'
import { Builder, Collection, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model'
import { ArrOf, Builder, Collection, Index, Model, Prop, TypeMarkup, TypeRef, TypeTimestamp, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import chunter from './plugin'
import contact, { Employee } from '@anticrm/contact'
export const DOMAIN_CHUNTER = 'chunter' as Domain
export const DOMAIN_COMMENT = 'comment' as Domain
@ -39,6 +40,18 @@ export class TMessage extends TDoc implements Message {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
@Prop(ArrOf(TypeRef(contact.class.Employee)), chunter.string.Replies)
replies?: Ref<Employee>[]
@Prop(TypeTimestamp(), chunter.string.LastReply)
lastReply?: Timestamp
@Prop(TypeRef(core.class.Account), chunter.string.CreateBy)
createBy!: Ref<Account>
@Prop(TypeTimestamp(), chunter.string.Create)
createOn!: Timestamp
}
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@ -95,7 +108,8 @@ export function createModel (builder: Builder): void {
addSpaceLabel: chunter.string.CreateChannel,
createComponent: chunter.component.CreateChannel
}
]
],
aside: chunter.component.ThreadView
}
}, chunter.app.Chunter)

View File

@ -13,7 +13,8 @@
// limitations under the License.
//
import type { Client } from '@anticrm/core'
import { Message } from '@anticrm/chunter'
import type { Client, Ref } from '@anticrm/core'
import core, { TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import chunter from './plugin'
@ -55,6 +56,19 @@ export async function createRandom (tx: TxOperations): Promise<void> {
}
}
export async function setCreate (client: TxOperations): Promise<void> {
const messages = (await client.findAll(chunter.class.Message, { })).filter((m) => m.createBy === undefined).map((m) => m._id)
if (messages.length === 0) return
const txes = await client.findAll(core.class.TxCreateDoc, { objectId: { $in: messages } })
const promises = txes.map(async (tx) => {
await client.updateDoc<Message>(chunter.class.Message, tx.objectSpace, tx.objectId as Ref<Message>, {
createBy: tx.modifiedBy,
createOn: tx.modifiedOn
})
})
await Promise.all(promises)
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
},
@ -62,5 +76,6 @@ export const chunterOperation: MigrateOperation = {
const tx = new TxOperations(client, core.account.System)
await createGeneral(tx)
await createRandom(tx)
await setCreate(tx)
}
}

View File

@ -25,7 +25,8 @@ import type { ViewletDescriptor } from '@anticrm/view'
export default mergeIds(chunterId, chunter, {
component: {
CommentPresenter: '' as AnyComponent,
ChannelPresenter: '' as AnyComponent
ChannelPresenter: '' as AnyComponent,
ThreadView: '' as AnyComponent
},
string: {
ApplicationLabelChunter: '' as IntlString,
@ -35,7 +36,9 @@ export default mergeIds(chunterId, chunter, {
Comment: '' as IntlString,
Message: '' as IntlString,
Reference: '' as IntlString,
Chat: '' as IntlString
Chat: '' as IntlString,
CreateBy: '' as IntlString,
Create: '' as IntlString
},
viewlet: {
Chat: '' as Ref<ViewletDescriptor>

View File

@ -20,7 +20,7 @@ import { readable } from 'svelte/store'
import Root from './components/internal/Root.svelte'
export type { AnyComponent, AnySvelteComponent, Action, LabelAndProps, TooltipAligment, AnySvelteComponentWithProps } from './types'
export type { AnyComponent, AnySvelteComponent, Action, LabelAndProps, TooltipAligment, AnySvelteComponentWithProps, Location } from './types'
// export { applicationShortcutKey } from './utils'
export { getCurrentLocation, locationToUrl, navigate, location } from './location'

View File

@ -20,6 +20,10 @@
"Message": "Message",
"Reference": "Reference",
"Chat": "Chat",
"In": "In"
"In": "In",
"Replies": "Replies",
"LastReply": "Last reply",
"RepliesCount": "{replies, plural, =1 {# reply} other {# replies}}",
"Thread": "Thread"
}
}

View File

@ -20,6 +20,10 @@
"Message": "Сообщение",
"Reference": "Ссылка",
"Chat": "Чат",
"In": "в"
"In": "в",
"Replies": "Ответы",
"LastReply": "Последний ответ",
"RepliesCount": "{replies, plural, =1 {# ответ} =2 {# ответа} =3 {# ответа} =4 {# ответа} other {# ответов}}",
"Thread": "Обсуждение"
}
}

View File

@ -14,26 +14,43 @@
-->
<script lang="ts">
import type { Ref, Space } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import type { Message } from '@anticrm/chunter'
import attachment from '@anticrm/attachment'
import contact,{ Employee } from '@anticrm/contact'
import { Ref,Space } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import chunter from '../plugin'
import { default as MessageComponent } from './Message.svelte'
import MessageComponent from './Message.svelte'
export let space: Ref<Space> | undefined
let messages: Message[] | undefined
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
const query = createQuery()
const employeeQuery = createQuery()
employeeQuery.query(contact.class.Employee, { }, (res) => employees = new Map(res.map((r) => { return [r._id, r] })))
$: updateQuery(space)
function updateQuery (space: Ref<Space> | undefined) {
if (space === undefined) {
query.unsubscribe()
messages = []
return
}
query.query(chunter.class.Message, {
space
}, (res) => {
messages = res
})
}
$: query.query(chunter.class.Message, { space }, result => { messages = result }, { lookup: { _id: { attachments: attachment.class.Attachment } }})
</script>
<div class="flex-col container">
{#if messages}
{#each messages as message}
<MessageComponent {message}/>
<MessageComponent {message} {employees} on:openThread />
{/each}
{/if}
</div>

View File

@ -15,56 +15,24 @@
<script lang="ts">
import type { IntlString } from "@anticrm/platform"
import { Label } from "@anticrm/ui"
export let title: IntlString
export let line: boolean = false
export let params: any = undefined
</script>
<div class="flex-center container" class:line={line}>
<div class="title">{title}</div>
<div class="w-full flex-center whitespace-nowrap mb-4">
<Label label={title} {params} />
<div class="ml-4" class:line={line} ></div>
</div>
<style lang="scss">
.container {
.line {
position: relative;
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);
}
}
height: 1px;
background-color: var(--theme-chat-divider);
}
</style>

View File

@ -14,35 +14,49 @@
-->
<script lang="ts">
import { generateId, Ref, Space } from '@anticrm/core'
import chunter from '../plugin'
import { getClient } from '@anticrm/presentation'
import Channel from './Channel.svelte'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import { Message } from '@anticrm/chunter'
import { generateId,getCurrentAccount,Ref,Space, TxFactory } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { getCurrentLocation,navigate } from '@anticrm/ui'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import Channel from './Channel.svelte'
export let space: Ref<Space>
const client = getClient()
const _class = chunter.class.Message
let _id = generateId()
let _id = generateId() as Ref<Message>
async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail
await client.createDoc(_class, space, {
const me = getCurrentAccount()._id
const txFactory = new TxFactory(me)
const tx = txFactory.createTxCreateDoc<Message>(_class, space, {
content: message,
createOn: 0,
createBy: me,
attachments
}, _id)
tx.attributes.createOn = tx.modifiedOn
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, space, chunter.class.Channel, _id, message)
_id = generateId()
}
function openThread (_id: Ref<Message>) {
const loc = getCurrentLocation()
loc.path[3] = _id
navigate(loc)
}
</script>
<div class="msg-board">
<Channel {space} />
<Channel {space} on:openThread={(e) => { openThread(e.detail) }} />
</div>
<div class="reference">
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/>

View File

@ -1,72 +1,107 @@
<!--
// 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 { Avatar, getClient } from '@anticrm/presentation'
import { AttachmentDocList } from '@anticrm/attachment-resources'
import type { Message } from '@anticrm/chunter'
// import { ActionIcon, IconMoreH } from '@anticrm/ui'
// import Emoji from './icons/Emoji.svelte'
import contact,{ Employee,EmployeeAccount,formatName } from '@anticrm/contact'
import { Account,Ref } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { Avatar,getClient,MessageViewer } from '@anticrm/presentation'
import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui'
import { getActions } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import chunter from '../plugin'
import { getTime } from '../utils'
// import Share from './icons/Share.svelte'
// import Bookmark from './icons/Bookmark.svelte'
import Bookmark from './icons/Bookmark.svelte'
import Emoji from './icons/Emoji.svelte'
import Thread from './icons/Thread.svelte'
import Reactions from './Reactions.svelte'
import Replies from './Replies.svelte'
import { MessageViewer } from '@anticrm/presentation'
import { getTime, getUser } from '../utils'
import { formatName } from '@anticrm/contact'
import { AttachmentList } from '@anticrm/attachment-resources'
import { WithLookup } from '@anticrm/core'
import { Attachment } from '@anticrm/attachment'
export let message: WithLookup<Message>
let reactions: boolean = false
let replies: boolean = false
let thread: boolean = false
export let message: Message
export let employees: Map<Ref<Employee>, Employee>
export let thread: boolean = false
const client = getClient()
const dispatch = createEventDispatcher()
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
let reactions: boolean = false
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, message, chunter.class.Message)
if (actions.length === 0) return
showPopup(
Menu,
{
actions: [
...actions.map((a) => ({
label: a.label,
icon: a.icon,
action: async () => {
const impl = await getResource(a.action)
await impl(message)
}
}))
]
},
ev.target as HTMLElement
)
}
async function getEmployee (createdBy: Ref<Account>): Promise<Employee | undefined> {
const account = await client.findOne(contact.class.EmployeeAccount, { _id: createdBy as Ref<EmployeeAccount> })
if (account === undefined) return
return employees.get(account.employee)
}
function openThread () {
dispatch('openThread', message._id)
}
</script>
<div class="container">
<div class="avatar"><Avatar size={'medium'} /></div>
<div class="message">
<div class="header">
{#await getUser(client, message.modifiedBy) then user}
{#if user}{formatName(user.name)}{/if}
{/await}
<span>{getTime(message.modifiedOn)}</span>
</div>
<div class="text"><MessageViewer message={message.content}/></div>
<div class="attachments"><AttachmentList {attachments} /></div>
{#if (reactions || replies) && !thread}
<div class="footer">
<div>{#if reactions}<Reactions/>{/if}</div>
<div>{#if replies}<Replies/>{/if}</div>
{#await getEmployee(message.createBy) then employee}
<div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div>
<div class="message">
<div class="header">
{#if employee}{formatName(employee.name)}{/if}
<span>{getTime(message.createOn)}</span>
</div>
{/if}
</div>
<!-- {#if !thread}
<div class="buttons">
<div class="tool"><ActionIcon icon={IconMoreH} size={'medium'}/></div>
<div class="tool"><ActionIcon icon={Bookmark} size={'medium'}/></div>
<div class="tool"><ActionIcon icon={Share} size={'medium'}/></div>
<div class="tool"><ActionIcon icon={Emoji} size={'medium'}/></div>
<div class="text"><MessageViewer message={message.content}/></div>
{#if message.attachments}<div class="attachments"><AttachmentDocList value={message} /></div>{/if}
{#if (reactions || message.replies)}
<div class="footer flex-col">
<div>{#if reactions}<Reactions/>{/if}</div>
{#if !thread}
<div>{#if message.replies}<Replies replies={message.replies} lastReply={message.lastReply} on:click={openThread} />{/if}</div>
{/if}
</div>
{/if}
</div>
{/if} -->
{/await}
<div class="buttons">
<div class="tool"><ActionIcon icon={IconMoreH} size={'medium'} action={(e) => { showMenu(e) }}/></div>
{#if !thread}
<div class="tool"><ActionIcon icon={Thread} size={'medium'} action={openThread}/></div>
{/if}
<div class="tool"><ActionIcon icon={Bookmark} size={'medium'}/></div>
<!-- <div class="tool"><ActionIcon icon={Share} size={'medium'}/></div> -->
<div class="tool"><ActionIcon icon={Emoji} size={'medium'}/></div>
</div>
</div>
<style lang="scss">
@ -106,36 +141,33 @@
margin-top: 1rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
height: 2rem;
align-items: flex-start;
margin-top: .5rem;
user-select: none;
div + div {
margin-left: 1rem;
margin-top: 0.5rem;
}
}
}
// .buttons {
// position: absolute;
// visibility: hidden;
// top: -.5rem;
// right: -.5rem;
// display: flex;
// flex-direction: row-reverse;
// user-select: none;
.buttons {
position: absolute;
visibility: hidden;
top: -.5rem;
right: -.5rem;
display: flex;
flex-direction: row-reverse;
user-select: none;
// .tool + .tool {
// margin-right: .5rem;
// }
// }
.tool + .tool {
margin-right: .5rem;
}
}
// &:hover > .buttons {
// visibility: visible;
// }
&:hover > .buttons {
visibility: visible;
}
&:hover::before {
content: '';
}

View File

@ -1,62 +1,80 @@
<!--
// 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 { Avatar } from '@anticrm/presentation'
import contact,{ Employee } from '@anticrm/contact'
import { Ref, Timestamp } from '@anticrm/core'
import { Avatar,createQuery } from '@anticrm/presentation'
import { Label, TimeSince } from '@anticrm/ui'
import chunter from '../plugin'
export let replies: string[] = ['Chen', 'Elon', 'Tim', 'Elon', 'Tim', 'Chen']
export let replies: Ref<Employee>[] = []
export let lastReply: Timestamp = new Date().getTime()
$: employees = new Set(replies)
let shown: number = 4
let showReplies: Array<string> = []
for (let i = 0; i < shown; i++) {
showReplies.push(replies[i])
const shown: number = 4
let showReplies: Employee[] = []
const query = createQuery()
$: updateQuery(employees)
function updateQuery (employees: Set<Ref<Employee>>) {
query.query(contact.class.Employee, {
_id: { $in: Array.from(employees) }
}, (res) => {
showReplies = res
}, {
limit: shown
})
}
</script>
<div class="flex-row-center container">
<div class="counter">{replies.length} Replies</div>
<div class="flex-row-center container cursor-pointer" on:click>
<div class="flex-row-center">
{#each showReplies as reply}
<div class="reply"><Avatar size={'x-small'} /></div>
<div class="reply"><Avatar size={'x-small'} avatar={reply.avatar} /></div>
{/each}
{#if replies.length > shown}
<div class="reply"><span>+{replies.length - shown}</span></div>
{#if employees.size > shown}
<div class="reply"><span>+{employees.size - shown}</span></div>
{/if}
</div>
<div class="whitespace-nowrap ml-2 mr-2 over-underline"><Label label={chunter.string.RepliesCount} params={{ replies: replies.length }} /></div>
{#if replies.length > 1}
<div class="mr-1">
<Label label={chunter.string.LastReply} />
</div>
{/if}
<TimeSince value={lastReply} />
</div>
<style lang="scss">
.container {
user-select: none;
.counter {
margin-right: .75rem;
line-height: 150%;
color: var(--theme-content-color);
white-space: nowrap;
}
border: 1px solid transparent;
border-radius: 0.5rem;
padding: 0.25rem;
.reply {
display: flex;
justify-content: center;
align-items: center;
width: 1rem;
height: 1rem;
background-color: var(--theme-bg-color);
border-radius: 50%;
margin-right: -.625rem;
span {
display: flex;
@ -71,10 +89,15 @@
background-color: var(--theme-bg-selection);
border-radius: 50%;
}
}
&:last-child {
margin-right: 0;
}
.reply + .reply {
margin-left: 0.25rem;
}
&:hover {
border: 1px solid var(--theme-button-border-hovered);
background-color: var(--theme-bg-color);
}
}
</style>

View File

@ -0,0 +1,174 @@
<!--
// 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 { AttachmentDocList } from '@anticrm/attachment-resources'
import type { Comment } from '@anticrm/chunter'
import contact,{ Employee,EmployeeAccount,formatName } from '@anticrm/contact'
import { Account,Ref } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { Avatar,getClient,MessageViewer } from '@anticrm/presentation'
import { ActionIcon,IconMoreH,Menu,showPopup } from '@anticrm/ui'
import { getActions } from '@anticrm/view-resources'
import chunter from '../plugin'
import { getTime } from '../utils'
// import Share from './icons/Share.svelte'
import Bookmark from './icons/Bookmark.svelte'
import Emoji from './icons/Emoji.svelte'
import Reactions from './Reactions.svelte'
export let comment: Comment
export let employees: Map<Ref<Employee>, Employee>
const client = getClient()
let reactions: boolean = false
const showMenu = async (ev: Event): Promise<void> => {
const actions = await getActions(client, comment, chunter.class.Comment)
showPopup(
Menu,
{
actions: [
...actions.map((a) => ({
label: a.label,
icon: a.icon,
action: async () => {
const impl = await getResource(a.action)
await impl(comment)
}
}))
]
},
ev.target as HTMLElement
)
}
async function getEmployee (createdBy: Ref<Account>): Promise<Employee | undefined> {
const account = await client.findOne(contact.class.EmployeeAccount, { _id: createdBy as Ref<EmployeeAccount> })
if (account === undefined) return
return employees.get(account.employee)
}
</script>
<div class="container">
{#await getEmployee(comment.modifiedBy) then employee}
<div class="avatar"><Avatar size={'medium'} avatar={employee?.avatar} /></div>
<div class="message">
<div class="header">
{#if employee}{formatName(employee.name)}{/if}
<span>{getTime(comment.modifiedOn)}</span>
</div>
<div class="text"><MessageViewer message={comment.message}/></div>
{#if comment.attachments}<div class="attachments"><AttachmentDocList value={comment} /></div>{/if}
{#if reactions}
<div class="footer">
<div><Reactions/></div>
</div>
{/if}
</div>
{/await}
<div class="buttons">
<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={Share} size={'medium'}/></div> -->
<div class="tool"><ActionIcon icon={Emoji} size={'medium'}/></div>
</div>
</div>
<style lang="scss">
.container {
position: relative;
display: flex;
margin-bottom: 2rem;
z-index: 1;
.avatar { min-width: 2.25rem; }
.message {
display: flex;
flex-direction: column;
width: 100%;
margin-left: 1rem;
.header {
font-weight: 500;
font-size: 1rem;
line-height: 150%;
color: var(--theme-caption-color);
margin-bottom: .25rem;
span {
margin-left: .5rem;
font-weight: 400;
font-size: .875rem;
line-height: 1.125rem;
opacity: .4;
}
}
.text {
line-height: 150%;
}
.attachments {
margin-top: 1rem;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
height: 2rem;
margin-top: .5rem;
user-select: none;
div + div {
margin-left: 1rem;
}
}
}
.buttons {
position: absolute;
visibility: hidden;
top: -.5rem;
right: -.5rem;
display: flex;
flex-direction: row-reverse;
user-select: none;
.tool + .tool {
margin-right: .5rem;
}
}
&:hover > .buttons {
visibility: visible;
}
&:hover::before {
content: '';
}
&::before {
position: absolute;
top: -1.25rem;
left: -1.25rem;
width: calc(100% + 2.5rem);
height: calc(100% + 2.5rem);
background-color: var(--theme-button-bg-enabled);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
z-index: -1;
}
}
</style>

View File

@ -0,0 +1,146 @@
<!--
// Copyright © 2021 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 attachment from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { Comment,Message } from '@anticrm/chunter'
import contact,{ Employee, EmployeeAccount } from '@anticrm/contact'
import { Class,generateId,getCurrentAccount,Lookup,Ref, Space } from '@anticrm/core'
import { createQuery,getClient } from '@anticrm/presentation'
import { IconClose,Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import ChannelSeparator from './ChannelSeparator.svelte'
import MsgView from './Message.svelte'
import ThreadComment from './ThreadComment.svelte'
const client = getClient()
const query = createQuery()
const messageQuery = createQuery()
const dispatch = createEventDispatcher()
export let _id: Ref<Message>
export let space: Ref<Space>
let message: Message | undefined
let commentId = generateId()
const lookup = {
_id: { attachments: attachment.class.Attachment }
}
$: updateQueries(_id)
function updateQueries (id: Ref<Message>) {
messageQuery.query(chunter.class.Message, {
_id: id
}, (res) => message = res[0], {
lookup
})
query.query(chunter.class.Comment, {
attachedTo: id
}, (res) => comments = res, {
lookup
})
}
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
const employeeQuery = createQuery()
employeeQuery.query(contact.class.Employee, { }, (res) => employees = new Map(res.map((r) => { return [r._id, r] })))
async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail
const employee = (getCurrentAccount() as EmployeeAccount).employee
await client.createDoc(chunter.class.Comment, space, {
attachedTo: _id,
attachedToClass: chunter.class.Message,
collection: 'replies',
message,
attachments
}, commentId)
await client.updateDoc(chunter.class.Message, space, _id, {
$push: { replies: employee },
lastReply: new Date().getTime()
})
// Create an backlink to document
await createBacklinks(client, space, chunter.class.Channel, commentId, message)
commentId = generateId()
}
let comments: Comment[] = []
</script>
<div class="header">
<div class="title"><Label label={chunter.string.Thread} /></div>
<div class="tool" on:click={() => { dispatch('close') }}><IconClose size='medium' /></div>
</div>
<div class="h-full flex-col">
<div class="content">
{#if message}
<div class="flex-col">
<MsgView {message} {employees} thread />
{#if comments.length}
<ChannelSeparator title={chunter.string.RepliesCount} line params={{ replies: message.replies?.length }} />
{/if}
{#each comments as comment}
<ThreadComment {comment} {employees} />
{/each}
</div>
{/if}
</div>
<div class="ref-input">
<AttachmentRefInput {space} _class={chunter.class.Comment} objectId={commentId} on:message={onMessage}/>
</div>
</div>
<style lang="scss">
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.75rem 0 2.5rem;
height: 4rem;
min-height: 4rem;
.title {
flex-grow: 1;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
user-select: none;
}
.tool {
margin-left: 0.75rem;
opacity: 0.4;
cursor: pointer;
&:hover {
opacity: 1;
}
}
}
.content {
display: flex;
flex-direction: column;
flex-grow: 1;
margin: 1rem 1rem 0px;
padding: 1.5rem 1.5rem 0px;
}
.ref-input {
margin: 1.25rem 2.5rem;
}
</style>

View File

@ -0,0 +1,13 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.1,2.9C11.8,1.5,9.9,0.7,8,0.7c-1.9,0-3.8,0.8-5.1,2.1C0.7,5,0.1,8.3,1.4,11c0.1,0.3,0.2,0.5,0.2,0.7 c0,0.2-0.1,0.5-0.2,0.8c-0.2,0.6-0.5,1.4,0.1,1.9c0.6,0.6,1.4,0.3,1.9,0.1c0.3-0.1,0.6-0.2,0.8-0.2c0.2,0,0.4,0.1,0.7,0.2 c1,0.4,2,0.7,3,0.7c1.9,0,3.8-0.7,5.2-2.1h0C16,10.3,16,5.7,13.1,2.9z M12.3,12.3c-1.8,1.8-4.5,2.3-6.9,1.2 c-0.4-0.2-0.8-0.3-1.2-0.3c-0.4,0-0.8,0.1-1.2,0.3c-0.2,0.1-0.6,0.2-0.7,0.2c0-0.1,0.1-0.5,0.2-0.7c0.1-0.4,0.3-0.8,0.3-1.2 c0-0.4-0.2-0.8-0.3-1.2C1.4,8.3,1.9,5.5,3.7,3.7C4.9,2.6,6.4,1.9,8,1.9s3.1,0.6,4.3,1.8C14.7,6.1,14.7,9.9,12.3,12.3L12.3,12.3z"
/>
<path d="M10.6,7.5L10.6,7.5c-0.4,0-0.8,0.3-0.8,0.8S10.2,9,10.6,9s0.8-0.3,0.8-0.8S11,7.5,10.6,7.5z" />
<path d="M8,7.5L8,7.5c-0.4,0-0.8,0.3-0.8,0.8S7.5,9,8,9s0.8-0.3,0.8-0.8S8.4,7.5,8,7.5z" />
<path d="M5.3,7.5L5.3,7.5c-0.4,0-0.8,0.3-0.8,0.8S4.9,9,5.3,9S6,8.7,6,8.3S5.7,7.5,5.3,7.5z" />
</svg>

View File

@ -23,6 +23,7 @@ import CommentInput from './components/CommentInput.svelte'
import CommentPresenter from './components/CommentPresenter.svelte'
import CommentsPresenter from './components/CommentsPresenter.svelte'
import CreateChannel from './components/CreateChannel.svelte'
import ThreadView from './components/ThreadView.svelte'
export { CommentsPresenter }
@ -33,7 +34,8 @@ export default async (): Promise<Resources> => ({
ChannelView,
CommentPresenter,
CommentsPresenter,
ChannelPresenter
ChannelPresenter,
ThreadView
},
activity: {
TxCommentCreate,

View File

@ -32,6 +32,10 @@ export default mergeIds(chunterId, chunter, {
ChannelDescription: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,
In: '' as IntlString
In: '' as IntlString,
Replies: '' as IntlString,
Thread: '' as IntlString,
RepliesCount: '' as IntlString,
LastReply: '' as IntlString
}
})

View File

@ -28,6 +28,7 @@
"dependencies": {
"@anticrm/platform": "~0.6.5",
"@anticrm/ui": "~0.6.0",
"@anticrm/contact": "~0.6.5",
"@anticrm/core": "~0.6.16"
}
}

View File

@ -13,7 +13,8 @@
// limitations under the License.
//
import type { AttachedDoc, Class, Doc, Ref, Space } from '@anticrm/core'
import type { Account, AttachedDoc, Class, Doc, Ref, Space, Timestamp } from '@anticrm/core'
import type { Employee } from '@anticrm/contact'
import type { Asset, Plugin } from '@anticrm/platform'
import { IntlString, plugin } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
@ -29,6 +30,10 @@ export interface Channel extends Space {}
export interface Message extends Doc {
content: string
attachments?: number
replies?: Ref<Employee>[]
lastReply?: Timestamp
createBy: Ref<Account>
createOn: Timestamp
}
/**

View File

@ -84,7 +84,7 @@
</Tooltip>
{/each}
</div>
{/if}
{/if}
<SearchEdit bind:value={search} on:change={() => {
dispatch('search', search)
}}/>

View File

@ -25,6 +25,7 @@
closeTooltip,
Component, getCurrentLocation,
location,
Location,
navigate,
PanelInstance,
Popup,
@ -60,6 +61,8 @@
let createItemLabel: IntlString | undefined
let navigatorModel: NavigatorModel | undefined
let asideId: string | undefined
onDestroy(
location.subscribe(async (loc) => {
closeTooltip()
@ -89,22 +92,14 @@
if (spaceId === currentSpace) {
// Check if we need update location.
const loc = getCurrentLocation()
if (loc.path[3] !== spaceSpecial) {
if (spaceSpecial !== currentSpecial && spaceSpecial !== asideId) {
if (spaceSpecial !== undefined) {
loc.path[3] = spaceSpecial
loc.path.length = 4
} else {
loc.path.length = 3
}
if (spaceSpecial !== undefined) {
loc.path[3] = spaceSpecial
loc.path.length = 4
specialComponent = getSpecialComponent(spaceSpecial)
currentSpecial = spaceSpecial
setSpaceSpecial(loc, spaceSpecial)
} else {
loc.path.length = 3
spaceSpecial = undefined
currentSpecial = undefined
asideId = undefined
}
navigate(loc)
}
@ -129,19 +124,33 @@
loc.path[2] = spaceId
loc.path.length = 3
if (spaceSpecial !== undefined) {
loc.path[3] = spaceSpecial
currentSpecial = spaceSpecial
loc.path.length = 4
specialComponent = getSpecialComponent(spaceSpecial)
setSpaceSpecial(loc, spaceSpecial)
}
navigate(loc)
} else {
asideId = undefined
currentView = undefined
createItemDialog = undefined
createItemLabel = undefined
}
}
function setSpaceSpecial (loc: Location, spaceSpecial: string): void {
loc.path[3] = spaceSpecial
loc.path.length = 4
specialComponent = getSpecialComponent(spaceSpecial)
if (specialComponent !== undefined) {
currentSpecial = spaceSpecial
asideId = undefined
} else if (navigatorModel?.aside !== undefined) {
asideId = spaceSpecial
} else {
loc.path.length = 3
currentSpecial = undefined
asideId = undefined
}
}
function selectSpecial (id: string): void {
specialComponent = getSpecialComponent(id)
if (specialComponent !== undefined) {
@ -247,6 +256,13 @@
limit: 1
}
)
function closeAside () {
const loc = getCurrentLocation()
loc.path.length = 3
navigate(loc)
asideId = undefined
}
</script>
{#if client}
@ -346,7 +362,9 @@
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}
</div>
<!-- <div class="aside"><Chat thread/></div> -->
{#if asideId && navigatorModel?.aside !== undefined}
<div class="antiPanel-component indent antiComponent filled"><Component is={navigatorModel.aside} props={{ currentSpace, _id: asideId }} on:close={closeAside} /></div>
{/if}
</div>
<PanelInstance />
<Popup />

View File

@ -54,6 +54,7 @@ export interface SpacesNavModel {
export interface NavigatorModel {
spaces: SpacesNavModel[]
specials?: SpecialNavModel[]
aside?: AnyComponent
}
/**